Skip to content

chore(core): UIPickerView's values and the wheel's pure math - #8156

Draft
vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-math
Draft

vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-math

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Base: main. #8526 (the player) and #8159 (the touch wheel) build on it.

User need

On a phone, people expect the date and time sheets' wheels to behave like the iOS picker. Today's wheel is a scroller with CSS snapping: a flick travels about a fifth as far as on iOS and can stop on a different row. This adds UIPickerView's measured values and the math behind its motion, for #8159's touch wheel; nothing changes on screen by itself.

Problem and solution fit

What led to it: Astryx's UIKit-parity work (spec:AST-094) asks the touch wheel to move like UIPickerView, and frame-by-frame recordings of UIKit's picker (iOS 26.2 simulator) showed three things a snapping scroller can't match: how fast a coast slows, where it settles, and the cylinder the rows sit on.

  • motion/uikit/picker.ts: UIPickerView's values (row and cylinder size, band, rubber band, stop speeds, tap spin), each with its source.
  • DateInput/wheelPhysics.ts: where a release comes to rest: UIKit's projected coast, snapped to a row, with the rate re-solved to stop exactly there; a slow release snaps, one past an end bounces back.
  • DateInput/wheelDrum.ts: where each row is drawn on the cylinder, and which row a tap lands on.

Current specification

spec:AST-094 FR1, FR3, FR4; design:reduced-motion DR2.

Public delta and caller burden

None: internal modules, unused until #8159.

Design and alternatives

  • Tune the native scroller. Cheapest, but the browser fixes a snapping scroller's deceleration and resting row, so it can't match iOS.
  • A general spring library. Flexible, but fitted to iOS case by case.
  • Chosen: UIKit's own model, from the recordings, because tests can hold it to them; the cost is about 1,000 lines and a table that must stay sourced. The compositor player is split out (chore(core): the wheel's compositor player #8526), so this is math a reviewer can check without a browser.

Evidence

The tests replay UIKit's recorded flicks, drags, end bounces and taps: coasts land within 0.8 pt of iOS, snaps within 0.4 pt.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

  1. Run pnpm exec vitest run packages/core/src/DateInput/wheelPhysics.test.ts packages/core/src/DateInput/wheelDrum.test.ts: all pass, each flick, slow drag and bounce compared with iOS's recorded frames.
  2. Without their fixes, two cases fail: a bounce spring that never settles runs forever instead of stopping at the animator's 30 s limit, and a tap on the fourth row's middle picks the third row.
  3. To see it move, flick a wheel in feat(Wheel): UIPickerView's motion under a finger, in today's look #8159's TimeInput story on a phone; this PR alone draws nothing.

Merge order: this PR → #8526 → #8159 (with #8320 and #8131) → #8162 → #8361 → #8366 → #8527 → #8480 → #8698.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 27, 2026 3:39am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 27, 2026, 4:28 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval needs an engineering owner's decision first: the wheel math rests only on the draft spec AST-094 that this branch adds, so an engineering owner has to approve or amend its FR1–FR3 and FR5, and its FR4, FR11 and FR14 need the current theme-tokens INV3, user-states DR1 and AST-025 FR14 updated or dropped from the draft; an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author, as an engineering owner, may do. Whatever that ruling, three changes are needed:

  • Land or rebase past the 17 stacked pull requests so this diff is only its own 1,971-line commit; as issued it merges 26,602 authored lines and an older AST-094 revision than the one in #8052.
  • Get lint green: eslint . --cache aborts with a JavaScript heap out-of-memory error at this head, while it passes on the base.
  • Project the release in wheelPhysics.ts with r ÷ (1 − r), as picker.ts documents and motion/projection.ts computes; the current constant lands 0.41–0.48 px past UIKit's recorded targets.

[Automated review]

if (speed <= stopSpeed) {
return offset;
}
return offset + (Math.sign(velocity) * (speed - stopSpeed)) / NORMAL_LAMBDA;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This projects with the continuous decay constant (÷ λ, 499.5 ms of release speed), but picker.ts:118-121 records UIKit's target as (v − stopSpeed) × r ÷ (1 − r) (499 ms), which projectDistance in motion/projection.ts already computes. On the three recorded releases in wheelPhysics.test.ts this lands 0.41–0.48 px past UIKit's targets, where the documented formula is within 0.15 px, and the 0.1 % error moves the chosen row on about 1.6 % of 500 px flicks. Suggest return offset + Math.sign(velocity) * projectDistance(speed - stopSpeed);, the matching header change at lines 27-28, and a 0.3 px tolerance in the projected-target test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 420f6ab: projectRelease now uses projectDistance (r ÷ (1 − r) ms of speed), and the header says so. The recorded-target test is tightened from 1 px to 0.3 px; it fails with the old constant (0.45 and 0.41 px off on two of the three recordings) and passes now.

Comment on lines +293 to +301
/** Release every resource; the rows stay where they are. */
dispose(): void {
const motion = this.motion;
if (motion != null) {
const offset = this.offset();
this.finish(motion);
this.restingOffset = offset;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After finish cancels the animations, the rows show the inline end state play wrote at lines 221-222 (curve.to), while restingOffset becomes the mid-flight offset, so the rows do not stay where they are and a later attach jumps them back. Calling this.render(offset, true) after finish would make the comment true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 420f6ab: dispose() renders the offset it captured after finish, so the rows stay where they are shown. New wheelMotion.test.ts disposes a turn 400 ms in and checks the strip shows that offset; it fails before the change (the strip showed the turn's end, -640 px).

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

DateInput (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 266B 266B 0B
Lines of Code 3532 4086 -
Complexity Very High Very High (390) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/DateInput 99.9KB 99.9KB 0B 14.8KB 14.8KB 0B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.6KB 34.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the draft spec AST-094 that this branch adds: the wheel math rests only on its FR1–FR3 and FR5, and its FR4, FR11 and FR14 would change the current theme-tokens INV3, user-states DR1 and AST-025 FR14, so an engineering owner needs to approve or amend those requirements by commenting /approve-spec <full head SHA> on this pull request, which the author, as an engineering owner, may do. Whatever that decision, three changes are still needed:

  • Land or rebase past the 17 stacked pull requests so this diff is only its own 1,983-line commit; as opened it merges 26,614 authored lines and an older AST-094 than the one in #8052.
  • Get lint green: ESLint runs out of JavaScript heap at this head, while the same job passes on main.
  • Project the release in wheelPhysics.ts with r ÷ (1 − r), as picker.ts documents and projectDistance computes; the current constant lands 0.41–0.48 px past UIKit's recorded targets.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the draft spec AST-094 that this branch adds: it is the only authority for this code, its FR4 forbids the theme-tuned touch motion the same head ships, and its FR11 and FR14 change current press and scroll-chaining rules. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner, or names the claims that must change. Whatever that decision, these changes are still needed:

  • Land or rebase past the 21 stacked pull requests so this diff is the wheel's own commit; as issued it merges 30,334 authored lines.
  • Make the adaptive compact test follow current AST-012 FR9 (width below the theme's md point, 768 px by default, or height below 500 px); at 744–767 px it opens the popover where FR9 asks for the sheet.
  • Drop the platform adaptation condition or add it to AST-012 FR2, whose list of condition fields is closed.
  • Stop astryx docs motion from saying sheets, menus and navigation already play the measured touch motion, since no component reads a motion role yet.
  • Project the wheel's release with r ÷ (1 − r), as picker.ts documents; the current constant lands 0.41–0.48 px past UIKit's recorded targets.

[Automated review]

Comment on lines +75 to +79
export const COMPACT_QUERY = '(width < 744px), (height < 500px)';

/** Both: presentations such as a sheet in place of a popover. */
export const COMPACT_TOUCH_QUERY =
'(width < 744px) and (pointer: coarse), (height < 500px) and (pointer: coarse)';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current AST-012 FR9 makes compact a width below the theme's effective md point (768 px by default) or a height below 500, with no resolver keeping size numbers of its own, and its DEC-6 rejects mapping compact to size classes. With 744 here, a coarse screen 744–767 px wide (an iPad mini in portrait) gets the popover where FR9 asks for the sheet, and a theme that moves md changes nothing. Suggest deriving compact from the theme's md and the 500 height edge, then updating the menu docs, the stories and touchPresentation.test.ts:328.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this pull request's commits: utils/touchPresentation.ts is #8076's. At #8076 1203525, which this branch now sits on, the resolver reads the theme's compact definition (DEFAULT_COMPACT_DEFINITION, AST-012 FR9) and keeps no numbers of its own.

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-012 FR2 limits a when object to width, height, pointer, contrast and motion, and says future fields need an explicit addition to that closed list. platform is not in it, so either drop it from this stack or bring the FR2 amendment for an engineering owner to approve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this pull request's commits: themeAdaptations.ts and its platform field come from #8076's stack. The FR2 amendment or the removal belongs to that change.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this head no component reads a motion role (only the new DateInput/wheel* files import the motion foundation), so this tells theme authors that sheets, menus and navigation already play the measured motion and that a retune takes effect, which it does not yet. Say the roles apply as touch presentations adopt them, or hold the section until one does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not this pull request's text: the roles section is #8076's. This pull request adds only the picker.tapSpin.durationMs row, and the picker wheel in #8159 reads it.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

  • Release velocity. core/src/DateInput/wheelPhysics.ts:116 estimates the finger's speed at lift with a local estimator. A shared velocity module is being specified so every gesture reads one velocity; please don't add another estimator here.

This is about where the behavior lives, not about the PR's intent.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

  • Release velocity: the local estimator goes: releaseVelocity and its 0.25 / 0.75 weight in motion/uikit/picker.ts. The wheel reads the pan recognizer's one velocity at lift, from the shared tracker, and will take the single release filter when AST-100 (spec(AST-100): one velocity tracker and one release filter for every gesture #8312) lands it. UIKit's recorded releases stay in the tests as data for that filter.

This lands in the restack onto #8076 (1203525). I'll add the commit here once it's pushed.

@vjeux
vjeux force-pushed the uikit/c21-wheel-math branch from eaac590 to 420f6ab Compare October 25, 2026 00:39
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 25, 2026
@github-actions
github-actions Bot requested a review from cvkxx October 25, 2026 00:40

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are needed before this can merge. The wheel's table, release math and drum follow spec:AST-094, but compared with main this pull request still carries the whole unmerged stack beneath it (31,519 authored lines, about 29,300 of them from #8053 through #8320), and the wheel's own 2,214 lines still need a split, for example the player (wheelMotion.ts) apart from the table, physics and drum.

  • WheelMotion.crossFade moves the rows in a setTimeout at the fade's midpoint (wheelMotion.ts:364-370), so on a busy main thread a reduced-motion wheel fades back in on the old row and then jumps; please bake the landing into compositor keyframes, as play() already does for its end state.
  • The wheel modules' headers still name Wheel.tsx, Wheel.test.tsx and PickerWheel.tsx as consumers or SYNC targets, and list a release-velocity export that no longer exists.

[Automated review]

Comment on lines +364 to +370
motion.timers.push(
window.setTimeout(() => {
if (this.motion === motion) {
onMidpoint?.();
this.render(to, true);
}
}, durationMs / 2),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rows land here on a main-thread timer while the fade itself runs on the compositor. If the main thread is blocked across the midpoint, the fade comes back in on the old row and the rows jump when the block ends, which is what design:reduced-motion DR2 ("An entering element appears in its final place and fades in") and AST-094 FR5 rule out. Writing the destination transforms inline first and giving each moved row and the strip a transform animation that holds the origin until 50% and then steps would keep the landing off the main thread, as play() does for its end state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c9ac0c, in #8526, where the player now lives: the destination transforms go inline first, and each moved row and the strip hold the origin to the fade's midpoint, then step onto it, on the compositor, as play() bakes its end. Without Web Animations (jsdom) the timer still lands them. The new unit case fails without the fix (with no timer run, the strip still showed the origin). #8162 adds a Chromium case that blocks the main thread for 700 ms across the midpoint: every frame of the fade coming back in has the tapped row at the band. On the build without the fix it fails, the row still 28 px below the band 187 ms in.

Comment on lines +7 to +11
* @output Exports the curves a picker wheel moves on: the release plan (a
* coast onto a row, a snap, a coast into an end and its bounce, a return
* from past an end), the tap spin, the rubber band while dragging, the
* release velocity and the row crossings that tick
* @position Pure math of the Wheel (Wheel.tsx); the drum (wheelDrum.ts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header still lists "the release velocity", which the module no longer exports, and names Wheel.tsx as the consumer. The head's Wheel.tsx is the scroll-snap column and imports none of these modules. The same stale consumer appears in wheelDrum.ts:11, wheelMotion.ts:11 and the SYNC lists, along with Wheel.test.tsx and PickerWheel.tsx (picker.ts:32), which do not exist at this head.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6cf08e9: wheelPhysics.ts no longer lists a release velocity, and wheelPhysics.ts, wheelDrum.ts and picker.ts name no consumer; their SYNC lists hold only files present here. #8159, where PickerWheel.tsx arrives and reads them, names it again (f069d97). wheelMotion.ts moved to #8526 with the player, its header naming the touch picker wheel.

@vjeux vjeux mentioned this pull request Oct 26, 2026
4 tasks done
@vjeux vjeux changed the title chore(core): UIPickerView's values, the wheel's pure math and its compositor player chore(core): UIPickerView's values and the wheel's pure math Oct 26, 2026
@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

On the review's points about this pull request's size:

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are needed before this can merge: the wheel's table, release math and drum follow spec:AST-094 and reproduce UIKit's recorded releases, but compared with main this pull request still carries the 15 unmerged pull requests beneath it (28,540 authored lines, 26,944 of them the stack's). Everything the wheel imports beyond its own files is already on main, so rebasing its commits onto main, or landing the stack first, leaves a 1,596-line diff.

  • Please also drop the release velocity from the uikit/picker.ts row of motion/README.md and from the wheelPhysics.test.ts header, which no longer describe what this pull request holds.

[Automated review]

Comment thread packages/core/src/motion/README.md Outdated
| `uikit/timing.ts` | Non-spring clocks and curves: press highlight, Core Animation's named curves, keyboard, Reduced Motion cross-fade. |
| `uikit/navigation.ts` | UINavigationController: push velocity, parallax, dimming, transition clock, interactive pop finish rule. |
| `uikit/gestures.ts` | Gesture recognizer thresholds, the velocity filter and haptic stand-ins, as `{value, unit}` entries (sources in JSDoc). |
| `uikit/picker.ts` | UIPickerView: the wheel geometry and band, the release velocity and its floor, the coast's stop speed, the snap at rest, the tap spin, the tick. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

picker.ts holds the velocity floor (release.minimumSpeed) but no release velocity: the new Picker section of MEASUREMENTS.md says the release weighting stays with the gesture system. Dropping “the release velocity and” from this row keeps it accurate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e3f1c20: the picker row lists the velocity floor without a release velocity, which stays with the gesture system.

Comment on lines +5 to +11
* @input wheelPhysics.ts, the gesture system's velocity tracker, and touch
* streams and per-frame offsets recorded from a UIPickerView
* @output Proves the Wheel's curves reproduce UIKit's measured picker: the
* release velocity, the projected target, the coast onto the row, the coast
* into an end, the snap, the rubber band, the tap spin and the ticks
* @position Unit coverage for DateInput/wheelPhysics.ts; the browser specs
* prove the same curves on screen

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header names the gesture system's velocity tracker as an input and says the file proves the release velocity and that browser specs prove the curves, but the file imports neither, and the recorded stream arrays are never read. Dropping those parts (and the unused stream fields), or naming where the velocity is proven, keeps the header true to what the file checks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a279dbf: the header names what the file reads (UIKit's recorded frames, with the velocity UIKit handed its delegate) and what it proves, without the velocity tracker or the browser specs, and the recordings' unread stream fields are gone.

vjeux added 2 commits October 26, 2026 06:09
motion/uikit/picker.ts holds UIPickerView's measured values, each sourced:
32 pt rows on a 97.2 pt cylinder at perspective 900, a 34 pt band inset 9 pt,
the band row magnified 23.5/21, dimmed rows at 0.447, a 291 pt rubber band,
the coast's stop speed and velocity floor, the snap at rest, a 0.3 s tap
spin and a 40 ms tick (iOS 26.2 simulator, real HID touches, Oct 23 2026).
Every value is an internal constant: spec:AST-094 FR4 names none of them, so
nothing registers this table (DEC-11). The UIKit index exports it, and
MEASUREMENTS.md and the motion README list it.
…IKit's recordings

wheelPhysics.ts plans a release as UIPickerView does: it projects the rest
at the normal deceleration rate (0.998), snaps it to a row and coasts with
the rate re-solved to stop there; a slow release snaps; a release past an
end bounces on scroll.bounce, and a bounce spring not settled by the
animator's 30 s limit snaps onto its end there. It also draws the rubber
band, the tap spin and the row crossings that tick. wheelDrum.ts puts rows
on the cylinder, finds the row under a point and bakes a curve into
per-row keyframes. Their tests replay UIKit's recorded releases: coasts
within 0.8 pt RMS, snaps within 0.4 pt, transforms within 0.3 degrees.
Nothing imports them yet; the touch picker wheel does.
Moving onto main carried an older header for the UIKit index: it dropped
the one constant per spring (SHEET_PRESENT_SPRING, …) that main's header
lists. The header is main's again, with `picker` added. The bounce
curve's comment no longer names motion/resolveMotion.ts, which is not on
main: its caller resolves the spring.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended for approval: the picker table, release math and drum follow spec:AST-094, and the release plan reproduces UIKit's recorded targets and coasts. Two small header fixes are worth folding in: motion/uikit/index.ts should keep listing the per-spring constants it still re-exports, and wheelPhysics.ts should drop its reference to motion/resolveMotion.ts, which does not exist. When the player and the picker wheel play the 300 ms tap spin and the scroll.bounce spring, AST-094 FR4 expects them to go through motion roles.

[Automated review]

Comment thread packages/core/src/motion/uikit/index.ts Outdated
Comment on lines +6 to +7
* @output Re-exports `springs`, `scroll`, `timing`, `navigation`, `picker` and
* the gesture thresholds (one `UIKIT_*` group per recognizer, their union

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header no longer mentions the per-spring constants the file still re-exports (ACTION_SHEET_DISMISS_SPRING … VIEW_CHANGE_SPRING, lines 43–79), which it listed before. Suggest keeping “one constant per spring (SHEET_PRESENT_SPRING, …)” and adding picker beside it.


/**
* The bounce spring, from `from` with `velocity`, onto `to`. `bounce` is
* `scroll.bounce`'s spring as the wheel resolves it (motion/resolveMotion.ts).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

motion/resolveMotion.ts does not exist on main or in this pull request, and registerScrollTable registers no role for scroll.bounce, so nothing resolves this spring. Suggest describing bounce as a spring the caller may hand, scroll.bounce by default; the same phrase recurs at line 312.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
# Conflicts:
#	packages/core/src/DateInput/wheelPhysics.test.ts
#	packages/core/src/DateInput/wheelPhysics.ts
#	packages/core/src/motion/README.md
#	packages/core/src/motion/uikit/index.ts

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested for one fix: the picker table and release math follow spec:AST-094 and reproduce UIKit's recorded releases, but rowAtPoint in DateInput/wheelDrum.ts ignores the 900 px perspective the drum is drawn through. Drawn as the module describes, the fourth row above and below the band sits 84.7–89.3 px from its centre, and rowAtPoint answers the third row for almost all of it, so a tap there would turn the wheel to the wrong row. Please make the hit test projection-aware (solve dy = R sin θ · p ÷ (p + R(1 − cos θ)) for θ before converting to a distance, or test each visible row's projected extent) with a test against projected positions, or leave rowAtPoint out until a drum consumer needs it. When the player and picker wheel play the 300 ms tap spin and the scroll.bounce spring, AST-094 FR4 expects them to go through motion roles.

[Automated review]

if (Math.abs(dy) >= geometry.radius) {
return null;
}
const distance = geometry.radius * Math.asin(dy / geometry.radius);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inverts the cylinder orthographically, but the dimmed copies are drawn through the picker's 900 px perspective (lines 18-19), which moves row centres 0.18, 1.31, 3.76 and 7.04 px toward the band for rows ±1 to ±4, not under a pixel as the comment above says. Drawn with rowOrigin and rowTransform in Chromium, the fourth row from the band spans 84.7–89.3 px and this returns the third row for about 89% of it, including its middle. Solving dy = R·sin θ · p / (p + R·(1 − cos θ)) for θ before converting to a distance (or hit-testing each visible row's projected extent) fixes it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5bb4b6: rowExtent gives where CSS draws a row (laid out at the band's centre, turned by rowTransform about rowOrigin's origin, seen through the perspective from the band's centre), and rowAtPoint answers the row drawn under the point: inside the band, the band's flat strip; outside it, the drum row whose drawn extent holds the point, the nearer one in the sliver between two rows, nothing past the outermost row. The tests compose the CSS transforms from the strings the drum hands CSS and sweep every 0.25 px of the column; the fourth row's middle now answers the fourth row (before: the third). In Chromium, painting the rows as the drum tells CSS to, every painted device pixel down the drum answers its row (2,866 of 2,869 at DPR 4, the other three on the seams between two rows), against 392 misses before.

…he perspective

rowAtPoint inverted the cylinder orthographically, but the rows above and
below the band are drawn through the picker's 900 px perspective, which
draws them nearer the band: the fourth row from the band spans 84.7-89.3 px,
and rowAtPoint answered the third row for 89% of it, middle included.
rowExtent now gives where CSS draws a row (laid out at the band's centre,
turned by rowTransform about rowOrigin's origin, seen through the
perspective from the band's centre), and rowAtPoint answers the row drawn
under the point: inside the band, the band's flat strip; outside it, the
drum row whose drawn extent holds the point, the nearer one in the sliver
between two rows, and nothing past the outermost row.
The tests compose the CSS transforms from the strings the drum hands CSS
and sweep every 0.25 px of the column at two offsets; without the fix the
fourth row's middle answered row 34 instead of 35. In Chromium, painting the
rows as the drum tells CSS to, every painted device pixel down the drum
answers its row (2,866 of 2,869 at DPR 4, the other three on the seams
between two rows), against 392 misses before.
@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

On the motion roles: the merged decision covers both values. Under #8257 the touch motion tables register only spec:AST-094 FR4's named roles, as internal roles, and every other table value stays an internal constant until the public set is decided (spec:AST-094/DEC-11). The tap spin's 300 ms is UIPickerView's measured turn, and scroll.bounce is UIScrollView's measured bounce; neither is one of FR4's named roles, so both play as internal constants. The touch wheel (#8159) resolves scroll.bounce and the tap spin's curve, FR4's timing.curves.move, through resolveMotion when each motion starts.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended for approval: the picker table, the release math and the drum follow spec:AST-094, the release plan reproduces UIKit's recorded targets and coasts, and rowAtPoint now answers the row drawn under a tap through the drum's perspective. One thing to settle before #8159 plays the plan on its 28 px rows: wheelPhysics.ts scales the measured velocity floor and stop speed with the row height, so the floor drops from 250 to 218.75 px/s there; please keep the speeds at their measured values or label the scaling a guess with a measurement task, as AST-094 FR1 and FR2 ask. Whether the 300 ms tap spin and scroll.bounce must be motion roles under AST-094 FR4 is for the pull request that plays them.

[Automated review]

curve: bounceCurve(offset, velocity, edge, scale, bounce),
};
}
const minimumSpeed = picker.release.minimumSpeed * 1000 * scale;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scales UIKit's measured 0.25 px/ms floor by the row height over 32, so #8159's 28 px rows get 218.75 px/s, and lifts between 219 and 250 px/s coast about four rows where the measured floor would snap them. AST-094 FR1 asks for the measured value by default and FR2 for a source or a guess label on any other; keeping the speeds unscaled (scaling only lengths such as the rubber-band dimension), or recording the scaling as a guess with a measurement task, would cover it. The stop speed at lines 199 and 301 scales the same way.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
The look's drum takes meta#8156's projection-aware hit test (rowExtent, and
rowAtPoint answering the row drawn under a point through the perspective),
keeping the look's own semantics: a position along the wheel, a copy at
every position of a looping wheel. Its drum tests add meta#8156's.

# Conflicts:
#	packages/core/src/DateInput/wheelDrum.ts
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
The look's drum takes meta#8156's projection-aware hit test (rowExtent, and
rowAtPoint answering the row drawn under a point through the perspective),
keeping the look's own semantics: a position along the wheel, a copy at
every position of a looping wheel. Its drum tests add meta#8156's.

# Conflicts:
#	packages/core/src/DateInput/__snapshots__/PickerWheel.test.tsx.snap
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
The look's drum takes meta#8156's projection-aware hit test (rowExtent, and
rowAtPoint answering the row drawn under a point through the perspective),
keeping the look's own semantics: a position along the wheel, a copy at
every position of a looping wheel. Its drum tests add meta#8156's.
# Conflicts:
#	packages/core/src/motion/README.md

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended for approval: the picker table, the release math and the drum follow spec:AST-094, and the release plan reproduces UIKit's recorded targets and coasts. One thing to settle in this file before #8159 plays the plan on its 28 px rows: wheelPhysics.ts scales the measured velocity floor and stop speed with the row height, so the floor drops from 250 to 218.75 px/s there; keep the speeds at their measured values, or label the scaling a guess with a measurement task, as AST-094 FR1 and FR2 ask. Whether the 300 ms tap spin and scroll.bounce must be motion roles under FR4 is for the pull request that plays them.

[Automated review]

curve: bounceCurve(offset, velocity, edge, scale, bounce),
};
}
const minimumSpeed = picker.release.minimumSpeed * 1000 * scale;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scales the measured 0.25 px/ms floor by the row height over 32, so a 28 px wheel (#8159's WHEEL_ROW_PITCH) coasts lifts from 218.75 px/s that UIKit's measured floor snaps; stopSpeed at lines 199 and 301 scales the same way. AST-094 FR1 asks for the measured value (and FR2 for a labelled guess with a measurement task otherwise), so consider keeping both speeds unscaled and scaling only the lengths that follow the drawn size.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
# Conflicts:
#	packages/core/src/motion/README.md
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
The look's drum takes meta#8156's projection-aware hit test (rowExtent, and
rowAtPoint answering the row drawn under a point through the perspective),
keeping the look's own semantics: a position along the wheel, a copy at
every position of a looping wheel. Its drum tests add meta#8156's.

# Conflicts:
#	packages/core/src/DateInput/wheelMotion.test.ts
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:32

This branch was successfully deployed

1 active deployment
Preview — e9656d8a Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants