npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

iphone-duo-expo-rn

v0.1.0

Published

The iPhone "Duo" frosted-glass fold effect for any Expo app - TypeScript only, no native code.

Readme


Install

npx expo install iphone-duo-expo-rn

Then add whichever optional pieces you want. All three ship with the Expo SDK, and the module works without any of them:

npx expo install expo-sensors expo-blur expo-linear-gradient

| Package | What it adds | Without it | | --- | --- | --- | | expo-sensors | The fold follows the device | Drive the tilt yourself with setManualTilt() | | expo-blur | The frosted blur | Warp and dimming only | | expo-linear-gradient | A smooth dimming ramp | Falls back to React Native gradients, then stepped bands |

Requires Expo SDK 57+ (React Native 0.82+). Runs on iOS, Android and web — and inside Expo Go, because there is no custom native code to link and no config plugin to add.

Use

Wrap your app once, as high in the tree as you can.

Expo Router

// app/_layout.tsx
import { DuoFoldProvider } from 'iphone-duo-expo-rn';
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <DuoFoldProvider>
      <Stack />
    </DuoFoldProvider>
  );
}

Prefer to leave the layout component alone? Use the HOC:

import { withDuoFold } from 'iphone-duo-expo-rn';

function RootLayout() {
  return <Stack />;
}

export default withDuoFold(RootLayout);

Plain Expo app

import { DuoFoldProvider } from 'iphone-duo-expo-rn';

export default function App() {
  return (
    <DuoFoldProvider>
      <YourApp />
    </DuoFoldProvider>
  );
}

Control it

import { useDuoFold, useDuoFoldStatus, useDuoFoldTilt } from 'iphone-duo-expo-rn';

function DebugPanel() {
  const fold = useDuoFold();
  const tilt = useDuoFoldTilt();             // sampled at 30 Hz by default
  const { hasSensor } = useDuoFoldStatus();

  return (
    <>
      <Text>{tilt.toFixed(1)}°</Text>
      <Button title="Recalibrate" onPress={fold.recalibrate} />
      <Button title="Hold at 30°" onPress={() => fold.setManualTilt(30)} />
      <Button title="Back to sensor" onPress={() => fold.setManualTilt(null)} />
      <Button title="Off" onPress={() => fold.setEnabled(false)} />
      {!hasSensor ? <Text>No motion sensor — manual only</Text> : null}
    </>
  );
}

useDuoFold() returns a stable controller, and reading from it never re-renders.

Gallery

The fold resolves its hinge from the sign of the tilt — it is never pinned to one side. The hinge edge stays sharp and anchored; everything away from it lifts, frosts and dims in proportion to how far the glass has separated from the plane behind it.

Tuning

Every knob mirrors the original implementation, expressed in millimetres and density-independent points instead of shader units.

<DuoFoldProvider
  parameters={{
    eyeDistanceMillimeters: 450, // larger = flatter perspective, less cropping
    blurSpread: 0.12,            // blur radius gained per dp of glass/plane gap
    darkening: 0.015,            // light lost per dp of blur radius
    maxTiltDegrees: 45,
  }}
  motion={{
    smoothing: 0.7,              // fraction of the error closed per sample
    recenterTauSeconds: 15,      // how fast the zero pose washes drift away
  }}
  blurBands={28}                 // strips approximating the continuous blur ramp
  blurUpdateHz={30}
  missColor="#000000"            // what the glass absorbs toward, and what a miss shows
/>

blurSpread is the one to reach for first. The default is faithful to the original, which is strong — past about 20° most of the screen is deliberately illegible. Halve it to 0.06 for a subtler effect that stays readable at any angle.

How it works

The original is a per-pixel AGSL shader. For each output pixel it treats the position as arc length along the tilted glass, casts a ray from a fixed eye through that point onto the UI plane behind, and samples there with a gap-proportional disk blur:

d     = |x - hingeX|                 // distance from the hinge, along the glass
glass = hingeX ± d · cos(tilt)       // the glass point, rotated about the hinge
gap   = d · sin(tilt)                // how far it lifted toward the eye
hit   = centre + (glass - centre) · E / (E - gap)

React Native has no per-pixel shader that can read the view tree behind it, so the effect is decomposed into three layers it can express.

Geometry — exact. That per-pixel map is a homography, so its inverse is one too, and it can be written in closed form as a scale, a rotation about the view's centre line under a camera, and a screen-space offset:

θ  = |tilt|,  h = hinge · width/2
tx = h(cos θ - 1) / cos θ
K  = (E - width·sin θ/2 + tx·hinge·sin θ) / (E·cos θ)

perspective = K·E      rotateY = -tilt
scaleX      = K/cos θ  scaleY  = (E·cos θ - width·sin θ/2) / (E·cos θ)
translateX  = tx

None of that is a hand-rolled matrix, deliberately: Android decomposes transforms into rotationY + cameraDistance + scale + 2D translation, so an arbitrary homography would not survive the trip. Every value above stays inside the subset iOS, Android and web reproduce identically. solveFoldTransform() is verified against the shader's own projection to machine precision in the test suite.

Dimming — exact. The shader's 1 - darkening · radius is linear in the distance from the hinge, so a single static gradient per side, scaled by an animated opacity, reproduces the entire curve.

Blur — approximated. A per-pixel variable-radius blur has no React Native equivalent, so the ramp is quantised into blurBands backdrop-blurred strips. Blur is a low-frequency cue, so the strips re-render at blurUpdateHz rather than at sensor rate. This is the one place where the port is a likeness rather than a reproduction.

Every value the renderer needs is a smooth function of the signed tilt, so it is baked once per layout into Animated interpolation tables driven by a single Animated.Value. A new sensor sample updates the views' native props directly: the geometry is never re-solved, React never re-renders, and the wrapped app never learns that the device moved. No react-native-reanimated required.

The motion model ports the original's sensor fusion: a calibrated zero pose, screen-axis remapping that honours display rotation, rotation-rate prediction to cover sensor latency, a light low-pass, and a slow washout that absorbs drift while the device is still. On web, where expo-sensors reports no fused attitude, it integrates the rotation rate instead and lets the washout hold the zero.

Notes and limits

  • The blur is banded, not per-pixel. Raise blurBands for a smoother ramp at the cost of more views, or pass blurEnabled={false} to drop it.
  • Content in its own window is not folded. React Native Modals, native alerts and system UI draw outside the provider's view, so they stay flat — usually what you want for a settings sheet.
  • Touch targets move with the pixels. Hit-testing follows the transform on every platform, so taps land where things look; at large tilts, that is a long way from where they were laid out.
  • No permission prompt on iOS or Android. The effect uses CoreMotion attitude and the Android rotation vector, neither of which is gated, so the module never calls requestPermissionsAsync() on its own — asking would raise a Motion & Fitness prompt it does not need. On web, call useDuoFold().requestPermissions() from a user gesture.
  • Android needs a blur backend. expo-blur renders a plain translucent view on Android unless told otherwise; this module passes blurMethod="dimezisBlurViewSdk31Plus" by default. Override it with the blurMethod prop.
  • Simulators have no motion sensors. useDuoFoldStatus().hasSensor reports false; drive setManualTilt() from a slider instead. The example app ships one.

API

| Export | What it is | | --- | --- | | DuoFoldProvider | Wraps an app in the fold; owns the sensor | | withDuoFold(Component, config?) | The same thing as an HOC | | DuoFoldView | The renderer, for folding one subtree under an existing provider | | useDuoFold() | Stable controller: recalibrate, setManualTilt, setEnabled, setAutoRecenterEnabled, requestPermissions, getState, subscribe | | useDuoFoldTilt(hz?) | Live tilt in degrees | | useDuoFoldStatus() | { hasSensor, source, enabled } | | useDuoFoldState(selector, hz?) | Any slice of the state | | solveFoldTransform() | The closed-form transform, if you want to drive it yourself | | foldBlurRadiusDp() · foldDarkenAlpha() · foldBlurStrength() | The blur and dimming curves | | FoldMotionModel | The sensor fusion, with platform plumbing lifted out | | DuoFoldEngine | Sensor subscription plus animated tilt value |

Example app

cd example
npx expo install
npx expo run:ios     # or: npx expo run:android

The example puts a ChatGPT-style transcript under the fold — long-form text on near-white is the least forgiving surface there is, so every degree of perspective and every seam in the blur ramp shows. The overflow (···) button opens the controls in the screenshots above; on a simulator, the slider is how you drive it.

Contributing

npm install
npm run lint
npm test
npm run build

The geometry is covered by property-style tests that check the solver against the reference projection across eye distances, tilts and hinge sides. If you change foldGeometry.ts, those should stay green without loosening a tolerance.

Author

Binni Cordova — 8 years building production mobile and web software.

I like problems like this one: take an effect that “needs” a shader, find the maths that makes it expressible in primitives every platform already has, and ship it as something another developer can install in one line.

Available for React Native, Expo and TypeScript work.

License

MIT © Binni Cordova