Skip to content

feat(mobile): start the splash purple and keep only the pixel dismiss - #1763

Merged
janicduplessis merged 2 commits into
mainfrom
feat/1739-purple-splash
Sep 28, 2026
Merged

janicduplessis merged 2 commits into
mainfrom
feat/1739-purple-splash

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Description

The native splash was icon.png at 120 pt on white (dark #15121D), so the overlay from #1714 first had to flood the icon's purple square across the screen before the pixel ripple could start. The splash now starts brand purple, so the flood goes away and the pixel dismiss is the whole animation.

Also fixes the edge strip: while the app settled from 0.98 to 1, a 1–2 px strip of the root view showed at the screen edges. With a purple native background it would turn purple, so the app now sits on its own theme background.

Native change, no OTA. The splash config and its image change, so the runtime fingerprint changes (iOS 1e83e2fc… -> 61b8fe9d…, Android 695c5f9d… -> f12ea3fd…). This ships in TestFlight build 16, together with the asset catalog change already on main (#1732). Build numbers come from EAS (appVersionSource: remote, autoIncrement), so nothing is bumped here.

Solution

Native splash. #5521FF in light and dark, with a new assets/images/splash-logo.png: icon.png with everything outside the white circle made transparent. It keeps the 1024 px canvas, so imageWidth: 120 and the on-screen circle (66 pt) are unchanged and the handoff geometry is the same as before.

Overlay. Its first frame is a full-screen grid of purple pixels with the logo on top, matching the native splash. Then, all as native EaseView animations from one render:

ms
0–90 logo pops to 1.2
90–220 logo shrinks to 0
100–560 pixels shrink with a 45° turn over 160 ms each, delayed by distance from the centre (0–300 ms), as in #1714
100–700 the app settles from 0.98 to 1

About 700 ms in total; the overlay unmounts at 610 ms.

The grid lives in its own component, PixelDismiss (src/components/pixel-dismiss.tsx), with the stagger as a pure rippleDelay(row, column, rows, columns, spreadMs) and a unit test. splash-overlay.tsx keeps the lifecycle: native handoff on onDisplay, the 300 ms fallback, once per process, Reduce Motion, useHardwareLayer. setOptions({ duration: 0 }) and the boot test's unmount assertion are unchanged.

Reduce Motion renders a solid purple fill plus the logo instead of the grid and crossfades it out over 250 ms. With Android's "Remove animations" (all three scales 0) the animators jump to their end, so the splash cuts straight to the app.

Edge strip. The overlay wraps the scaled app in a view with theme.colors.background from Unistyles, so it follows the in-app theme setting.

Native checks after prebuild:

  • iOS: the storyboard uses expo-splash-screen's own SplashScreenLogo imageset, separate from the React Native asset catalog (RCTUseAssetCatalog), which only serves the overlay's copy of the logo.
  • Android 12+: the 66 dp circle sits well inside the 192 dp icon mask, so nothing is cropped.

Test plan

Release builds (stim ios --configuration Release, stim android --variant release), cold starts. The long launch waits caused by host load are trimmed from the iOS light and Android animations-off clips.

Light Dark Reduce Motion
iOS 27 sim (iPhone 18 Pro)
ios-light.mp4
ios-dark.mp4
ios-reduce.mp4
Android emulator (1080x2400)
android-light.mp4
android-dark.mp4
android-reduce.mp4

Android with all animation scales at 0:

android-animations-off.mp4

Reduce Motion: defaults write com.apple.Accessibility ReduceMotionEnabled on iOS, settings put global transition_animation_scale 0 on Android.

Frame-by-frame checks on the extracted frames:

  • Handoff seam. iOS: the last native-splash frame and the first overlay frame differ only in the status bar clock; the circle spans the same pixels (504–701 on row 1311). Android: consecutive frames across the handoff differ by at most 3/255 in luminance; the circle spans x 454–625 throughout, and the top and bottom rows stay purple.
  • Edge strip. During the settle, the outermost columns match the app background (dark: rgb(19,16,28) on iOS), with no purple.
  • Android Reduce Motion. The crossfade plays over about 170 ms of captured frames. In two earlier runs, with the host's load average between 700 and 950, the emulator stalled for about 2 s at the handoff and the fade was not captured. I'm treating that as host load, not an app issue.

Fixes #1739

@janicduplessis janicduplessis left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review: clear

I read issue #1739, the diff against origin/main, the new test and the PR description. In apps/mobile I ran pnpm test (29 suites, 222 tests), pnpm run typecheck and pnpm run lint. All three pass.

Correctness

  • Unmount timer and timing. The overlay unmounts at 100 + (300 + 160) + 50 = 610 ms. The corner pixel's delay stays below SPREAD_MS, because the distance to a pixel's centre is less than the distance to the grid's corner. So the last pixel finishes by about 545 ms, and the logo finishes by 220 ms. The whole dismiss takes about 700 ms, including the app settle, which runs outside the overlay. That is inside the 650-800 ms target.
  • Handoff. It still runs through onDisplay on the logo Image, with the 300 ms fallback. played is set when the splash is hidden, so the dismiss plays once per process, and a remount starts in the started state. The first overlay frame is a full purple grid (colors.light.brand = #5521FF, the same colour as the config) with the 120 pt logo centred on top, which matches the native splash.
  • Grid coverage. rows is even and rows * size >= height. The grid is centred vertically and is exactly width wide, and the +1 overlap covers the gaps left by fractional sizes.
  • Reduce Motion. It renders a solid purple fill and the logo. dismissed stays false, so nothing scales. The overlay fades out over 250 ms and unmounts at 300 ms, and the app jumps to scale 1 with type: 'none'.
  • Theme background. The backdrop uses Unistyles theme.colors.background behind the 0.98-scaled app. It follows the in-app theme, and the parent tree still lays out with flex: 1.
  • Requirements kept. useHardwareLayer is still on the overlay, and the boot-test unmount assertion is unchanged and passes.

Non-blocking notes

  1. pixel-dismiss.tsx:51-52: the grid size comes from useWindowDimensions(), and the grid no longer has the extra margin rows the old flood grid had. Where the Android window height differs from the overlay's real size (3-button navigation, split screen, foldables), a strip at the top or bottom would show the app on the first frame instead of purple. The clips cover a 1080x2400 emulator. A cold start with 3-button navigation would confirm it. If a strip shows, size the grid from the container's onLayout instead of the window.
  2. splash-overlay.tsx:14-16: SPLASH_COLOR and LOGO_SIZE duplicate '#5521FF' and imageWidth: 120 from app.config.ts, and only the doc comment links them. That is acceptable, but a shared constant would keep them from drifting.
  3. pixel-dismiss.tsx:13: "until the last pixel is gone" is an upper bound, because the outermost pixel starts a little before SPREAD_MS. Consider "at most" for readers who reuse the constant.

Readability and comment policy. PixelDismiss is small and self-contained. It takes only color, dismissed and delay, the grid layout is easy to follow, rippleDelay is pure and has a test for its symmetric output, and the constants are named. The remaining comments are public API doc comments or name an external constraint (the splash config values, sub-pixel seams, when the native animations start). I found no violations.

@janicduplessis
janicduplessis marked this pull request as ready for review September 28, 2026 15:23
@janicduplessis
janicduplessis merged commit 44420d9 into main Sep 28, 2026
1 check passed
@janicduplessis
janicduplessis deleted the feat/1739-purple-splash branch September 28, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phone app: purple native splash with the pixel dismiss only

1 participant