iOS 27 tab bar: measured Liquid Glass motion, vibrancy, lens optics and native geometry - #5906
shai-almog wants to merge 14 commits into
Conversation
…nd native geometry The iOS 27 Tabs (tabsMorphPreset ios27) now reproduces UITabBarController from measurements taken with an instrumented native probe (motion-probe) rather than by eye: - TabGlassMotion / TabGlassGesture: the selection motion as measured 60 Hz tables, plus held presses, a press on the selected tab, drag scrubbing with release to the tab under the finger, and the release wobble. - Graphics.colorMatrixRegion (vibrant tab content, the selection platter) and Graphics.glassLensRegion (the lifted lens's refraction, outline, rim and shadow), with core reference implementations, JavaSE, iOS/macOS Metal and (colorMatrixRegion) JavaScript port support. - Native bar geometry in points and the native label face. - iOS Metal: the bar glass runs on the GPU, mutable-image drawing reuses its stencil, the tab motion is no longer painted twice per frame, and the lens scratch texture is no longer leaked. - Tooling: tabmotion.py regenerates every table; record-ios-tabs-side-by-side.sh records native and Codename One side by side under the same real touches (docs/videos). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Native fidelity (Android, Material 3)54 pairs compared -- median 95.6%, worst 91.3% ( Distribution --
Geometry vs native (bbox offset / size ratio / center offset / corner radius) -- gated separately from the visual score
Side-by-side comparisons (worst first)
|
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 157 screenshots: 157 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
Cloudflare Preview
|
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
|
Compared 172 screenshots: 172 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 172 screenshots: 172 matched. |
|
Compared 172 screenshots: 172 matched. |
|
Compared 193 screenshots: 193 matched. |
Native fidelity (iOS Modern, Metal)68 pairs compared -- median 95.0%, worst 83.5% ( Distribution --
Geometry vs native (bbox offset / size ratio / center offset / corner radius) -- gated separately from the visual score
Side-by-side comparisons (worst first)
|
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
Native fidelity (ios-27-metal)68 pairs compared -- median 94.6%, worst 74.5% ( Distribution --
Geometry vs native (bbox offset / size ratio / center offset / corner radius) -- gated separately from the visual score
Side-by-side comparisons (worst first)
|
GlassLensBlend uses MathUtil.exp (core compiles against CLDC11 in the Ant build). tabmotion.py opens every file through a context manager; all regenerated tables and fixtures are byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Compared 12 screenshots: 12 matched. |
…tables The light alpha is 1 - e - 0.3 (0.5 - e) sum(1-t) / |1-t|^2, the exact value that maps a white backdrop to the tint; the grey ramp is 5/16 / q and (19/16 g - 1/4) / q with q = 1 + 0.05 g (1 - g). Every matrix the probe logged matches to 5e-6 (the table reproduced them to 0.0046). tabmotion.py vibrancy now checks captures against the forms instead of fitting tables, and the test asserts native matrix entries to 2e-5. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rals UIKit's platter colorMatrix is a 1.2 gain less a share of the Rec. 709 luminance (0.33 dark, 0.07 light) plus an offset; every read-back entry matches within 1.4e-4, as close as UIKit's own rows agree. A test pins the formula to the native readback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Compared 166 screenshots: 166 matched. Benchmark Results
Detailed Performance Metrics
|
|
Compared 154 screenshots: 154 matched. Benchmark Results
Detailed Performance Metrics
|
…ables On the vsync clock (every captured frame renders at exactly start + n/60 s; the logged times carry main-thread jitter) seven of the fourteen tables are UIKit springs, written in closed form with DampedSpring: - travel: duration 0.4 s, bounce 0.15, ended past the overshoot once less than 0.195 pt remains - lift: critically damped 0.25 s rise and fall (fall ends 13.5 frames in) - platter: critically damped 0.4 s from 1 - rise and its velocity at the release (ends 28.5 frames in) - glow: layer critically damped 0.1 s to 0.8445, retargeted to 0 on a 0.5 s spring at the tap's touch-up; mask = 0.33675 * layer; each hidden under 0.005; scale 1 + 3 * the 0.5 s spring The deformation (STRETCH_*, LEAD_*) and the whole-bar pulse keep their tables: no closed form matched the capture as well. tabmotion.py check now holds the closed forms to the capture and fails beyond tolerance; java only splices the kept tables; fixture is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The settle is TabGlassMotion's travel spring (POSITION_OMEGA, POSITION_ZETA, now package-private so both share one definition) started from the position and velocity the follow spring leaves the lens with when the release reaches it, one touch lag after the finger lifts. It replaces the SETTLE_V/SETTLE_E tables and the position-curve correction: scrub centre RMS 0.517 -> 0.346 pt, worst 7.47 -> 3.44 pt; lift worst 0.42 -> 0.14. Fitted to the releases alone the spring comes out at 15.5 rad/s, zeta 0.847, against 15.708 / 0.85. The release wobble and the scrub deformation kernels stay tables; a note above the kernels records the structure a closed form would have to meet. The bar press spring uses DampedSpring. tabmotion.py gesture splices the wobble and deformation tables as before and replays the settle over the episodes gesture-fixture exports (--settle), failing beyond tolerance. The fixture and the splice are byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tabmotion.py java regenerates the remaining tables byte for byte again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Frames 3, 5, 7 and 17 (lift and glow onset, lift release) drifted 0.5-1.3% of pixels: the lens outline moves by a sub-pixel and the vibrant edge label recolours with the glow. The other 29 frames and every fidelity row are unchanged; strict verification passes on the iOS 27 simulator. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f72cd42555
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…testing - iOS: colour matrix and glass lens need the Metal renderer; GL and watchOS builds report no support so Tabs paints its regular content. - The glass motion applies to TOP and BOTTOM tabs only; LEFT and RIGHT keep the regular rendering. - An empty glass bar paints its glass without resolving a tab. - Right to left, the first glass tab sits at the right end. - The glass tabs overlap (each is as wide as its resting lens), so taps and scrub releases both pick the tab whose centre is nearest. - The pointer listeners stay registered with swiping off; they also drive the lens and skip the swipe themselves. - Metal (iOS and Mac): a colour-matrix region that runs off a screen edge keeps its texture coordinates and mask anchored to the full region. - JavaScript: colorMatrixRegion ignores the user transform, as on the other ports (Tabs scales its regions itself). Each new test was checked to fail with its fix reverted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dddf02237d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- A screen effect (blur, glass, lens, colour matrix) ends the Metal encoder mid-frame and opens another. That reset the transform to identity and dropped the rectangular and polygon clips the Java side had already sent, so the effect and everything after it drew unclipped. CN1Metalcompat now carries them across the restart (the polygon is re-stamped, as each pass starts with a cleared stencil) until presentFramebuffer ends the frame; clips set while drawing into a mutable image are not the screen's. - The vibrant tab content groups the tabs by their own tint in each state, one mask and matrix per group, so a tab with its own colour or a disabled tab keeps it. The usual single tint is still two masks. - Three TabsGlassMotion frames reseeded: with the transform and clip now kept, the lens rim and the vibrant edge label move by a few edge pixels. - The watchOS Core Graphics build is the only one without these ops. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The first tab could gain focus while it was briefly narrower than its title (before the glass layout ran), which started a Label ticker that never stopped: the title showed as a drifting 'ed', and every TabsGlassMotion frame differed from run to run by 0.15-0.4% of pixels -- the source of the intermittent iOS 27 frame-golden failures. UITabBar never scrolls a title, so the glass layout stops and disables the ticker on its tabs. All 36 frames reseeded with 'Featured' whole and centred; two runs of the same build now capture them pixel-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mid-flight the iOS 26 selection drop is taller than the bar and bulges past its edges, as UIKit's does, but tabsContainer's paint is clipped to the bar. On Metal the bulge only showed because an encoder restart dropped the clip; with the clip now carried across restarts it was cut at the bar edge. The drop's lens now widens the clip to the whole Tabs while in flight -- the morph repaints the whole Tabs every frame then -- and leaves it alone at rest, when the drop is inside the bar. The test implementation records lensRegion calls with their clip, and a test holds the morph mid-flight and checks the clip admits the bulge, up to the Tabs and no further. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Compared 223 screenshots: 223 matched. |
|
Compared 150 screenshots: 150 matched. |
at() tested rel == Float.MAX_VALUE. ParparVM writes that constant as the double 3.4028235E38, which a C float never equals, so the test was always false on iOS and Xcode 27 reports it (-Wliteral-range), failing the native warning census of build-ios-metal-27. releaseS answers the sentinel exactly when the finger is still down, so test upS < 0 instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96b0afff38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…g; JavaSE clip - A press only drives the glass lens when the component that receives it is the tab bar or one of its tabs, not an overlay (side menu, layered-pane sheet) covering the bar. - A theme switch between the ios26 and ios27 presets reinstalls the tab layout when the glass motion flips, so neither renderer runs over the other's layout. - Button's forced glass styles keep Component's isUnselectedStyle bookkeeping, so Label no longer measures a selected title with the width it cached for the unselected font. - JavaSE: lensRegion, glassLensRegion and colorMatrixRegion write back only inside the graphics clip (mapped into buffer pixels), as the Metal and JavaScript paths do; a raw setRGB bypassed it. Each new test was checked to fail with its fix reverted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Compared 155 screenshots: 155 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|




















































































































































































































































































































































































iOS 27 tab bar: measured Liquid Glass motion, vibrancy, lens optics and native geometry
The iOS 27
Tabs(tabsMorphPreset: "ios27") now reproduces the nativeUITabBarControllerfrom measurements instead of by eye: the selection motion, press/hold/drag gestures, vibrant content, the lifted glass lens and the bar's geometry.Side by side, native on top and Codename One below, driven by the same real touches on the iOS 27 simulator (six taps, a held press, a tap on the selected tab, a slow drag and a flick):
docs/videos/tabs-side-by-side-light-bar.mp4docs/videos/tabs-side-by-side-dark-bar.mp4How it was measured
scripts/fidelity-app/ios-native-ref/motion-probeis an instrumented native app: a realUITabBarControllerdriven by XCUITest with genuine taps, holds and drags, logging the presentation geometry of every layer under the bar on every display frame, plus KVC readback of the filters UIKit installs and lossless screenshots over known backdrops.scripts/fidelity-app/tools/tab-motion/tabmotion.pyfits the model from those logs and regenerates every table in the Java sources byte for byte (fit,java,gesture,vibrancy, and their fixtures).What changed
Motion (
TabGlassMotion,TabGlassGesture)Rendering
Graphics.colorMatrixRegion: recolours what is already painted under a shape and optional mask. Used for vibrant tab content (glyphs take their colour from the glass behind them,VibrancyMatrix: exact in dark, tabulated in light) and for the selection platter (the bar's own glass through UIKit's colour matrix).Graphics.glassLensRegion: the optics of the lifted lens (circular-bevel refraction that bends the surface around the rim into it, dispersion, outline, rim highlight, end shading, shadow), with the press brightening of the whole bar. Reference implementations in core (ColorMatrixBlend,GlassLensBlend).colorMatrixRegion.Display.getDevicePixelRatio()): 62 pt pill sized to its tabs (86 pt a tab), 21 pt from the screen edges, 94 x 54 pt resting lens; label font matched to the native ink widths.Performance (iOS, Metal)
glassScreenRegion) now runs entirely on the GPU instead of a CPU blur with two GPU syncs per call; the output matches the CPU path (all 68 fidelity rows unchanged).Tabs.animateno longer asks the form to repaint the whole component.Measured on the simulator, frames drawn in the first 0.6 s of a gesture went from about 11 of 36 to about 31 (native: about 33).
Tests and checks
TabGlassMotionTest,TabGlassGestureTest(replays 21 native holds and scrubs frame by frame),TabsGlassSelectionTest,VibrancyMatrixTest,ColorMatrixBlendTest; JS port parity and replay tests for the new op.TabsGlassMotionframe goldens are new and seeded; the baseline is updated. Only tab rows moved:Tabsdark 85.89, light 86.12;TabsGeomdark 93.67, light 92.99;TabOnedark 97.36, light 96.90. The other 62 rows are unchanged.scripts/ios/screenshots-metal-27/Tabs*) will need reseeding from CI.Known differences from native
glassLensRegionis not implemented in the JavaScript port yet (the lens is drawn without its optics there).🤖 Generated with Claude Code