Repository navigation
spec(AST-100): one velocity tracker and one release filter for every gesture - #8312
Conversation
…s stay per component New system spec: one Core tracker owns pointer samples and reports px/s; one release filter (window plus a stillness reset) is the only lift velocity a component reads; flick thresholds stay internal per component; event-clock timestamps for touch, pen, and mouse; direction from the caller's axis after the right-to-left check.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
|
There was a problem hiding this comment.
This record needs an engineering owner's decision before it can be approved: it adds new release-velocity requirements (FR1–FR5, DEC-1), and the open draft spec:AST-094 in #8060, which the stacked gesture pull requests also carry, specifies the opposite reading in its FR17 (UIKit's estimate, never decayed through a rest). An engineering owner, including the author as one, records the decision by commenting /approve-spec <full head SHA> on this pull request, and that decision should also settle which record owns release velocity and the event-clock rule. Separately from that decision, a few fixes are needed:
- Restate FR1, FR2 and FR5 as observable outcomes and move the single-tracker, sample-buffer and window wording to an architecture record, as
architecture:knowledge-contractsINV18 requires for product specs. - In FR5, cite
design:bidirectionalityDR13 andarchitecture:internationalizationINV11 instead of restating them and naming an unowned "shared right-to-left check". - In Platform support, cite
spec:AST-013FR1 together with FR3, and align the evidence line with the Verification table.
[Automated review]
| - **FR1 — One tracker owns the samples.** Core has one velocity tracker. A | ||
| gesture feeds it the contact's positions with their event timestamps, and it | ||
| reports velocity in CSS pixels per second on each physical axis. Every Core | ||
| and Lab component that reads the velocity of a pointer gesture uses it, | ||
| including a sheet's drag, a toast's swipe, a row swipe, paging, a wheel | ||
| picker, a navigation pop, and interactive dismiss. No component keeps its own | ||
| sample buffer or velocity math. | ||
| - **FR2 — One release filter.** The velocity a component reads when the | ||
| contact lifts comes only from the tracker's release filter: it uses the | ||
| samples within a fixed window before the newest sample, and reports 0 when | ||
| the contact stayed still for longer than a fixed stillness interval before | ||
| lifting. A component reads no other estimate and selects no alternative | ||
| filter. The window and the stillness interval are internal tuning values. |
There was a problem hiding this comment.
FR1 and FR2 name an internal module ("Core has one velocity tracker"), a data structure ("No component keeps its own sample buffer") and the filter algorithm ("the samples within a fixed window before the newest sample"), while Non-goals keep the tracker internal. architecture:knowledge-contracts INV18 keeps such mechanisms out of product specs unless they are an intentional public contract. Suggest stating the observable outcomes here (the same pointer stream gives the same release velocity in every gesture component; a pause longer than the stillness interval before the lift reads 0; the reading does not change with handler delay or pointer type) and moving single-tracker ownership to an architecture record listed in affects_architecture.
| - **FR5 — Direction comes from the caller's axis.** The tracker reports | ||
| physical x and y. A caller that acts along a logical axis maps the velocity | ||
| onto that axis after resolving the element's direction with the shared | ||
| right-to-left check, so a flick toward the inline end means the same in both | ||
| directions. |
There was a problem hiding this comment.
This outcome is design:bidirectionality DR13 ("A gesture toward logical next/end MUST move toward logical next/end in both directions"), and direction resolution belongs to architecture:internationalization INV11 ("a lazy DOM read"). Citing both, rather than restating them and naming an unowned "shared right-to-left check", keeps one owner per fact (architecture:knowledge-contracts INV10).
| - Supported feature/engine floor: the package's supported browsers | ||
| (`spec:AST-013` FR3); pointer events with event timestamps. |
There was a problem hiding this comment.
spec:AST-013 FR3 lists only the two explicit browser rows (latest desktop Chrome and iOS Safari); the supported tiers are FR1 (Baseline full support and Baseline minus two years). Suggest citing FR1 and FR3 here, and aligning the evidence line below (unit tests "prove FR1–FR5") with the Verification table, which proves FR1 with a source boundary check and FR3 and FR5 with component tests.
…tion-modality INV10; cite bidirectionality DR13 and internationalization INV11
There was a problem hiding this comment.
This pull request needs an engineering owner’s decision before it can be approved: it adds new release-velocity requirements in spec:AST-100 (FR1–FR5, DEC-1) and a new architecture:interaction-modality INV10, and the open draft spec:AST-094 in #8060 specifies the opposite release reading in FR17 and one shared threshold table in FR1. An engineering owner, including the author as one, records the decision by commenting /approve-spec <full head SHA> on this pull request, and that decision also settles which record owns release velocity. Separately from that decision, three fixes are needed:
- FR1 covers Lab gestures, but INV10 keeps the only velocity source unexported in
@astryxdesign/coreand forbids local velocity math, so a Lab gesture cannot conform; scope both to Core or say how Lab reaches the source. architecture:interaction-modalitystill says it owns only modality-path and stale-outcome requirements and names no velocity source; update its Purpose and Owning code.- AST-100’s FR1 verification row, and the FR2 and FR3 failure cells, still test INV10’s mechanism; prove FR1 with one stream read in two components and leave the source-boundary check to INV10.
[Automated review]
| - **FR1 — One velocity for every gesture.** Every Core and Lab component that | ||
| reads the velocity of a pointer gesture, including a sheet's drag, a toast's | ||
| swipe, a row swipe, paging, a wheel picker, a navigation pop, and interactive | ||
| dismiss, reports the same velocity, in CSS pixels per second on each physical | ||
| axis, for the same contact motion. `architecture:interaction-modality` INV10 | ||
| owns the single source of that velocity. |
There was a problem hiding this comment.
FR1 covers every Lab gesture (including a navigation pop) and names INV10 as the single source, but INV10 keeps that source unexported in @astryxdesign/core and forbids a component its own velocity math. @astryxdesign/lab reaches Core only through published subpaths, so a Lab gesture cannot conform either way. Scope FR1 and INV10 to Core, or say in INV10 how a Lab component reaches the source.
| - **INV10 — Gesture velocity has one owner.** In `@astryxdesign/core`, one | ||
| internal velocity tracker owns a gesture's pointer samples and reports | ||
| velocity, and its one release filter, a fixed sample window with a reset to | ||
| 0 after a still contact, is the only lift velocity a component reads | ||
| (`spec:AST-100`). It is not exported. No component keeps its own sample | ||
| buffer, velocity math, or alternative release filter; a component's flick | ||
| threshold compares the tracker's release velocity. |
There was a problem hiding this comment.
Two things to reconcile with this invariant: “It is not exported” leaves no path for the Lab gestures AST-100 FR1 covers, and the record’s Owning code still says it “owns only the cross-cutting modality-path and no-stale-interaction-outcome requirements” (with a Purpose to match), so it never names the velocity source it now owns.
|
|
||
| | Contract | Verification | Representative states | Mutation or failure expectation | | ||
| | -------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | ||
| | FR1 | Source boundary check over Core and Lab plus tracker unit tests | each gesture component; a new gesture component; steady motion in x and y | A component with its own sample buffer or velocity math; a unit other than pixels per second | |
There was a problem hiding this comment.
This row restates INV10’s source-boundary check: it fails “A component with its own sample buffer or velocity math”, which an implementation that gives the same reading would pass under FR1 and the Non-goals line on equivalent implementations. Suggest proving FR1 with one recorded stream read in two components and leaving the boundary check to INV10’s row.
…y; observable FR1–FR3 verification
There was a problem hiding this comment.
This pull request needs an engineering owner’s decision before it can be approved: it adds new release-velocity requirements in spec:AST-100 (FR1–FR5, DEC-1) and architecture:interaction-modality INV10, now including the rule that each package outside Core keeps a matching local tracker, and the open draft spec:AST-094 in #8060 specifies the opposite release reading in FR17 and one shared threshold table in FR1. An engineering owner, including the author as one, records the decision by commenting /approve-spec <full head SHA> on this pull request, and that decision also settles which record owns release velocity. Separately from that decision, two fixes are needed:
- AST-100’s FR1 (“INV10 owns the single source”), Intent, DEC-1 and Current-state impact still describe one Core source, while INV10 now gives each package outside Core its own tracker; align them with INV10, or drop the Lab rule and say how Lab reaches Core’s tracker.
- Platform support says “source-boundary” tests in Verification prove FR1–FR5, but AST-100’s Verification no longer lists one; drop that word.
[Automated review]
| axis, for the same contact motion. `architecture:interaction-modality` INV10 | ||
| owns the single source of that velocity. |
There was a problem hiding this comment.
INV10 now says packages outside Core “cannot import the tracker” and keep “one local tracker with the same release filter, window, and stillness interval”, so a Lab gesture’s velocity does not come from a single source. Suggest saying here that the velocity is the INV10 release filter’s reading (Core’s tracker, or the one local tracker a package outside Core keeps), and aligning the Intent (“One velocity source in Core owns the samples”), DEC-1 (“One tracker with one release filter”) and the Current-state impact line on later gestures.
| - Browser evidence: none required by this record; the unit, source-boundary, | ||
| and component tests in Verification prove FR1–FR5, and real-device feel for |
There was a problem hiding this comment.
AST-100’s Verification no longer lists a source-boundary check (FR1 is now proved with one recorded stream read through two components); the only one is INV10’s row in architecture:interaction-modality. Suggest dropping “source-boundary,” here.
…source plus matching Lab tracker
|
One question on FR2, with UIKit measurements (iOS 26.2 simulator, UIKit app). Edited: added UIKit replay. Should the one filter be UIKit's measured blend rather than the window filter FR2 names (a fixed window plus a reset after a fixed stillness interval)? What UIKit does:
How far the window filter is from it, on the four UIPickerView swipes replayed through each filter (px/s):
The window filter more than doubles a quick flick's release. A flick's projected distance scales with velocity, so paging and dismiss choices near their thresholds would come out differently. Until this is answered, the gesture recognizers ship UIKit's blend, with its constant fixed by recognizer kind: a pan reports |
There was a problem hiding this comment.
This pull request needs an engineering owner's decision before it can be approved: it adds new release-velocity requirements in spec:AST-100 (FR1–FR5, DEC-1) and architecture:interaction-modality INV10, and the open draft spec:AST-094 in #8316 reads release velocity the opposite way in FR17 and keeps every gesture threshold in one shared table in FR1, so that decision also settles which record owns release velocity. An engineering owner, including the author as one, records the decision by commenting /approve-spec <full head SHA> on this pull request. Separately from that decision, one fix is needed:
- INV10's last sentence ("Packages outside Core … keeps one local tracker with the same release filter"), with AST-100's Intent and DEC-1, also reaches the native package, where
spec:AST-082FR11 has native components use the platform's own control or convention; scope it to Lab (or the web packages) as FR1 and INV10's Verification row already do, or reconcile it with AST-082 if native platforms are meant.
[Automated review]
| threshold compares the tracker's release velocity. Packages outside Core | ||
| cannot import the tracker; a gesture there keeps one local tracker with the | ||
| same release filter, window, and stillness interval, so it reports the same | ||
| velocity for the same motion. |
There was a problem hiding this comment.
As written, this sentence also covers @astryxdesign/native, a package outside Core, where spec:AST-082 FR11 has a native component use "the platform's own control or convention" for sheets, navigation and scrolling, so its release velocity would be the platform's, not this filter's. AST-100 FR1, its Platform support (browsers, pointer events) and this record's INV10 Verification row ("a Lab-to-Core comparison") all stop at Core and Lab. Suggest scoping the sentence to Lab (or to the web component packages) and aligning AST-100's Intent ("every Astryx component") and DEC-1 ("wherever a package cannot reach it") with it; if native platforms are meant, say so in AST-100 FR1 and reconcile it with AST-082 FR11.
AST-100 now specifies iOS's blend with a stillness reset: a contact still for longer than the stillness interval before lifting releases at 0 (meta#8312). Pan, scroll-view pan, pinch and rotation pass the lift's event time to the tracker, which reads 0 when no sample came within velocity.stillness: 40 ms, Android's ASSUME_POINTER_STOPPED_TIME (AOSP VelocityTracker), since iOS has no time cutoff and keeps the last motion. A lift while moving keeps the blend's value, so the recorded iOS releases are unchanged. The constants table accepts an AOSP source.
There was a problem hiding this comment.
This pull request needs an engineering owner's decision before it can be approved: spec:AST-100 (FR1–FR5, DEC-1) and architecture:interaction-modality INV10 add new release-velocity requirements, FR2's stillness reset departs from the UIKit reading measured in this pull request's comments, and draft spec:AST-094 still reads velocity (FR17) and thresholds (FR1) the other way, so the decision also settles which record owns them. An engineering owner, including the author as one, records it by commenting /approve-spec <full head SHA> on this pull request. Separately from that decision, two wording fixes are needed:
- Scope the sameness to one kind of surface in the architecture record's Purpose ("every Core and Lab gesture reads the same release velocity") and its Deciding specs line, and in AST-100's FR1 and FR3 failure cells, which a correct wheel-and-sheet pair would fail.
- The Intent says "a flick of the same speed does the same thing in each of them", but FR3 keeps thresholds per component; say that components share the reading, not the outcome, and state DEC-1's harm the same way.
[Automated review]
| intent remains authoritative over stale work, and every Core and Lab gesture | ||
| reads the same release velocity. This record defines those shared interaction invariants, the |
There was a problem hiding this comment.
This still says every Core and Lab gesture reads the same release velocity, while AST-100 FR1 and FR2 now give dragged and scrolling surfaces different readings: on moved intervals of 500, 1,000 and 1,500 px/s, INV10 gives a sheet 1,100 px/s and a wheel picker 725 px/s. Suggest "every Core and Lab gesture of a kind reads the same release velocity", and the same scope in the Deciding specs line ("decides one release velocity for every gesture").
| | -------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ||
| | FR1 | One recorded sample stream read through two different components, plus a unit check | a sheet drag and a row swipe fed the same stream; a Lab gesture and a Core gesture fed the same stream; steady motion in x and y | Two components reporting different velocities for the same stream, or a unit other than pixels per second | | ||
| | FR2 | Release velocity tests through a component over recorded iOS streams | recorded pan and scroll-view releases; acceleration before lift; a pause longer than the stillness interval before lift; one sample | A release that differs from the velocity iOS recorded for the same stream, or a non-zero release after a still pause | | ||
| | FR3 | Component tests at each threshold | a flick just under and just over a component's threshold; the same velocity across two components | Two components acting on different velocities for the same flick, or a threshold reachable from public API | |
There was a problem hiding this comment.
With per-kind readings, a wheel picker and a sheet fed the same flick act on 725 and 1,100 px/s, both correct under FR2, but this row fails "Two components acting on different velocities for the same flick". Suggest "two components of the same kind" here, and the same in FR1's failure cell (line 119), whose "a Lab gesture and a Core gesture" names no kind.
| their finger, every Core and Lab component of that kind reads the same speed | ||
| from that motion, as iOS does, so a flick of the same speed does the same thing | ||
| in each of them, and a finger that |
There was a problem hiding this comment.
FR3 keeps flick, fling and commit thresholds per component, so one speed can still fire a row's action (0.6 px/ms today) and leave a toast in place (1.2 px/ms). Suggest saying that every component of a kind judges a flick from the same speed, and stating DEC-1's harm (lines 132-133) as a disagreement in the reading.
…re cells; components share the reading, not the outcome
|
/approve-spec 0d6f631 |
There was a problem hiding this comment.
Changes requested on one line: the release-velocity contract in spec:AST-100 and architecture:interaction-modality INV10 is owner-approved at this head, but AST-100's Platform support says no browser evidence is needed and that unit tests prove FR4, which conflicts with the current spec:AST-013 FR10 (real-browser evidence for browser event timing). FR4's promise that a late handler and the pointer type leave the velocity unchanged depends on the timestamps browsers put on pointer events, so please require real-browser evidence for FR4 in that line and in FR4's Verification row. That edit is a new commit, so an engineering owner, the author included, then records approval again by commenting /approve-spec <full head SHA>.
[Automated review]
| - Browser evidence: none required by this record; the unit and component | ||
| tests in Verification prove FR1–FR5, and real-device feel for | ||
| each component remains that component's evidence. |
There was a problem hiding this comment.
This line says unit tests prove FR4 and no browser evidence is needed, but FR4 (lines 79-81) promises that a late handler does not change the velocity and that touch, pen and mouse give the same velocity for the same motion, which depends on the timestamps browsers put on pointer events. spec:AST-013 FR10 allows unit tests only for guards and deterministic state and requires real-browser evidence for browser event timing, and a newer record cannot silently override it (architecture:knowledge-contracts INV9). Suggested wording, with the matching evidence added to the FR4 Verification row (line 122):
- Browser evidence: FR4 reads browser event timestamps, so real-browser
evidence shows that a delayed handler and touch, pen, and mouse input leave
the release velocity unchanged (`spec:AST-013` FR10); unit and component
tests prove FR1–FR3 and FR5, and real-device feel for each component remains
that component's evidence.
| threshold compares the tracker's release velocity. Lab cannot import the | ||
| tracker; a Lab gesture keeps one local tracker with the same blend and | ||
| stillness interval, so it reports the same velocity for the same motion. |
There was a problem hiding this comment.
Non-blocking: nothing routes this Lab rule to a Lab path. The record's interaction route (line 39) lists INV1–INV4, INV7 and INV8, applies_to covers Core only, and spec:AST-100's path-applicability names BottomSheet, Item and Toast, so pnpm specs:for packages/lab/src/Chart/ChartZoom.tsx lists neither INV10 nor AST-100. Adding INV10 to the interaction route would put it in front of the first Lab gesture review.
|
/approve-spec 9a6e9a4 |
There was a problem hiding this comment.
This spec can be approved as written: spec:AST-100 and architecture:interaction-modality INV10 are owner-approved at this head, and the browser-evidence line now asks for real-browser evidence for FR4, as spec:AST-013 FR10 requires. Nothing else needs to change before merge. Two optional follow-ups would tighten it: adding INV10 to the record's interaction route would put the Lab rule in front of the first Lab gesture review, and FR2's "the one case where it differs from iOS" could also name Platform support's fewer-than-two-moved-samples zero, unless iOS reads 0 there too.
[Automated review]
| reports for the same motion: a pan's velocity for a dragged surface, and a | ||
| scroll view's release velocity for a scrolling surface. It is 0 when the | ||
| contact stayed still for longer than a fixed stillness interval before | ||
| lifting, the one case where it differs from iOS. Every component of a kind |
There was a problem hiding this comment.
Non-blocking: Platform support (lines 92-93) also gives a gesture with fewer than two moved samples a release velocity of 0, and the FR2 Verification row (line 122) checks its "one sample" state against "the velocity iOS recorded". Unless iOS reads 0 there too, this sentence names only one of two departures; something like "…before lifting; this and Platform support's fewer-than-two-moved-samples rule are where it differs from iOS" would keep FR2 and its test consistent.
|
/approve-spec c112133 |
AST-100 now specifies iOS's blend with a stillness reset: a contact still for longer than the stillness interval before lifting releases at 0 (meta#8312). Pan, scroll-view pan, pinch and rotation pass the lift's event time to the tracker, which reads 0 when no sample came within velocity.stillness: 40 ms, Android's ASSUME_POINTER_STOPPED_TIME (AOSP VelocityTracker), since iOS has no time cutoff and keeps the last motion. A lift while moving keeps the blend's value, so the recorded iOS releases are unchanged. The constants table accepts an AOSP source.
There was a problem hiding this comment.
This spec can be approved as written: spec:AST-100 and architecture:interaction-modality INV10 are owner-approved at this head, and every current record they cite agrees with them, including the real-browser evidence for FR4 that spec:AST-013 FR10 requires. Nothing needs to change before merge. Two optional follow-ups would tighten it: adding INV10 to the modality record's interaction route would put the Lab rule in front of the first Lab gesture review, and FR2's “the one case where it differs from iOS” could also name Platform support's fewer-than-two-moved-samples zero, unless iOS reads 0 there too.
[Automated review]
|
Measured data on FR2's stillness reset next to a UIKit scroll view, for the record. No change is proposed here. Setup. #8320's FR4 page ran in Mobile Safari 26.2 on the iOS 26.2 simulator, fed the exact HID streams the UIKit measurements used. Each stream ran 4× in Safari and 4× in UIKit in the same session. The UIKit side was a full-screen pan view, and a full-screen two-axis Stream. Eleven 150 pt drags at 250 pt/s, at 0–90°, each ending in a 200 ms still hold before the lift.
So after a still hold, FR2's reset agrees with UIKit's scroll view more often than UIKit's pan does. The scroll view isn't consistent here, though (32 of 44). In an earlier measurement, a 1000 pt/s drag kept its fling after 500–1000 ms silent rests, so speed or the last interval seems to matter to it. Every stream that lifts at its last sample matches UIKit within the replay's run-to-run spread (FR4). Those numbers are in #8320's Testing section. |
AST-100 now specifies iOS's blend with a stillness reset: a contact still for longer than the stillness interval before lifting releases at 0 (meta#8312). Pan, scroll-view pan, pinch and rotation pass the lift's event time to the tracker, which reads 0 when no sample came within velocity.stillness: 40 ms, Android's ASSUME_POINTER_STOPPED_TIME (AOSP VelocityTracker), since iOS has no time cutoff and keeps the last motion. A lift while moving keeps the blend's value, so the recorded iOS releases are unchanged. The constants table accepts an AOSP source.
AST-100 now specifies iOS's blend with a stillness reset: a contact still for longer than the stillness interval before lifting releases at 0 (meta#8312). Pan, scroll-view pan, pinch and rotation pass the lift's event time to the tracker, which reads 0 when no sample came within velocity.stillness: 40 ms, Android's ASSUME_POINTER_STOPPED_TIME (AOSP VelocityTracker), since iOS has no time cutoff and keeps the last motion. A lift while moving keeps the blend's value, so the recorded iOS releases are unchanged. The constants table accepts an AOSP source.
AST-100 now specifies iOS's blend with a stillness reset: a contact still for longer than the stillness interval before lifting releases at 0 (meta#8312). Pan, scroll-view pan, pinch and rotation pass the lift's event time to the tracker, which reads 0 when no sample came within velocity.stillness: 40 ms, Android's ASSUME_POINTER_STOPPED_TIME (AOSP VelocityTracker), since iOS has no time cutoff and keeps the last motion. A lift while moving keeps the blend's value, so the recorded iOS releases are unchanged. The constants table accepts an AOSP source.
AST-100 now specifies iOS's blend with a stillness reset: a contact still for longer than the stillness interval before lifting releases at 0 (meta#8312). Pan, scroll-view pan, pinch and rotation pass the lift's event time to the tracker, which reads 0 when no sample came within velocity.stillness: 40 ms, Android's ASSUME_POINTER_STOPPED_TIME (AOSP VelocityTracker), since iOS has no time cutoff and keeps the last motion. A lift while moving keeps the blend's value, so the recorded iOS releases are unchanged. The constants table accepts an AOSP source.
Exact decision needed
Which code decides how fast a finger was moving when it lifted, for every Astryx gesture, and how components use that speed.
Existing authority searched
spec:AST-057(row actions): fling velocity is internal tuning (Not public line); no shared estimator.spec:AST-002FR3: thresholds and tuning are not public concepts.component:BottomSheet,component:Toast: no velocity rule.AST-100is the next number not used by a record onmainor an open specification pull request.spec:AST-094(in feat(core): pan gesture recognizer with UIKit's hysteresis and velocity #8060, not authority) FR17 reads release velocity as UIKit's estimate, never decayed through a rest. This record decides the opposite (FR2 stillness reset) and owns release velocity; AST-094 FR17 aligns to it or cites it before that draft is approved.architecture:interaction-modalityINV10, so the system spec states observable outcomes (architecture:knowledge-contractsINV18).packages/core/src/gestures/VelocityTracker.ts.Decision
New system spec
spec:AST-100:architecture:interaction-modalityINV10.Rejected alternatives
Claim scope and non-goals
Release velocity only. What each component does with it (thresholds, commit points, projection, settle) stays with the component; pinch velocity and browser scroll physics are outside.
Evidence and implementation relationship
The tracker is not on
mainyet: #8060 adds it, so this record is the contract its first landing must meet. At #8060's head the tracker offers four selectable strategies; its default is iOS's pan blend and never decays after a pause. FR2 keeps that blend, adds the scroll-view release blend and a stillness reset, and drops the other strategies. Onmain, BottomSheet (two-point window, event timestamps), Item's row swipe (px/ms on the handler clock), and Toast (whole-drag average on the wall clock) each estimate velocity their own way; Current-state impact moves them onto the tracker. The rebuild set above is separate implementation work. No implementation is included.iOS measurements behind FR2 (iOS 26.2 simulator, UIKit app, contributed in review): UIPanGestureRecognizer's
velocity(in:)is0.2·v(latest interval) + 0.8·v(previous interval), updated only on moved samples, and fits every value UIKit reported on six recorded streams to 0.1 pt/s. A scroll view's release blends its pan's last two values once more,0.25·pan(n) + 0.75·pan(n−1), reproducing four recorded UIPickerView releases to 0.03%. Neither decays through a silent rest. Replayed through a 100 ms window, the same four releases read 2,285.7 vs 933.4, 2,285.9 vs 2,114, 979.1 vs 1,016.2 and 1,160.9 vs 543.6 px/s. FR2 follows the blend and adds the stillness reset, so it differs from iOS only when a finger rests before lifting.Writing rubric results
Scope
Validation
pnpm check:knowledge(CI lint)git diff --check