iPhone Duo Fold Animation: Hinge Sensor, AGSL Shader and the Galaxy Z Fold 8 Recreation
The iPhone Duo fold animation is a user interface transition on Apple's first foldable handset, announced on 9 September 2026 and shipping from 23 October 2026. As the device opens or closes, the wallpaper and interface progressively blur, darken and appear to pass through the moving half of the display before resolving. It does not advance against a clock. It advances against the measured angle of the hinge, which is why it tracks the hand instead of a timer.
Within a day of the announcement a developer published a working recreation on a Samsung Galaxy Z Fold 8, built as a standalone Android app that reads the hinge sensor, renders on both displays through the Presentation API and drives an AGSL runtime shader from the current hinge value. The demo uses captured screenshots of the inner and outer home screens rather than the live launcher, a limitation the developer stated plainly and which press coverage repeated.
1. Hardware context
The iPhone Duo pairs a 7.6 inch inner Super Retina XDR panel with a nano-texture finish and a 5.4 inch outer panel covering roughly ninety percent of the iPhone 18 Pro screen area. Both run ProMotion and Always On at up to 3,000 nits. The chip is the A20 Pro: six CPU cores, seven GPU cores, a dual 16-core Neural Engine, cooled by a vapour chamber, with Apple claiming up to thirty-five percent better sustained performance than the iPhone 17 Pro. The hinge uses more than one hundred components. Each half carries its own battery. Pricing starts at 1,999 dollars, pre-orders 16 October 2026, software iOS 27.1.
Two details matter for the animation. Sustained GPU headroom, because a per-pixel blur across a 7.6 inch panel at 120 Hz is not cheap for the length of a hinge movement. And the hinge itself, because a transition claiming to track geometry is only convincing if the reported angle is smooth and low latency. A coarsely quantised sensor produces stepping no shader can hide.
2. Observable behaviour
The transition decomposes into five simultaneous effects. None is individually novel. The combination, all five parameterised by one continuous physical value, produces the impression of a single bending surface.
- Progressive blur concentrated near the fold, strongest around the midpoint.
- Darkening across the same band, reading as a shadow in the crease.
- Partial transparency, so the moving half looks like something you see through.
- Perspective compression, content narrowing toward the hinge as the panel rotates away.
- A cross-dissolve between closed-state and open-state content.
The crucial property is reversibility. Stopping partway freezes the effect at the matching state. Reversing direction reverses the effect. A time-based animation cannot do this, because its progress variable has no relationship to where the device physically is.
3. The hinge angle as a control signal
3.1 Sensor availability
Android has exposed a hinge angle sensor since API level 30. Sensor.TYPE_HINGE_ANGLE reports the angle between the two halves in degrees as an ordinary event stream. Range and rest values differ between devices, so production code reads them from the sensor object rather than assuming 0 to 180.
On macOS the equivalent is the internal lid angle sensor, reachable as an IOHIDDevice with vendor 0x05AC and product 0x8104. The macTilt project polls it at 60 Hz and exposes configurable start and end angles, for example beginning at 80 degrees and completing at 3 degrees. Apple does not document that device, so the approach is reverse engineered rather than supported.
3.2 Normalisation
p = clamp( (theta - theta_closed) / (theta_open - theta_closed), 0, 1 )
p = 0 closed, p = 0.5 halfway, p = 1 open. Keeping normalisation in one place means the shader holds no device-specific constants and runs unchanged on hardware with a different hinge range.
Raw readings jitter, and a jittering uniform shimmers visibly in the blur band. A small exponential filter usually suffices:
p_smooth = p_smooth + a * (p_raw - p_smooth), a about 0.25
Too much smoothing and the effect lags the hand, which destroys the illusion more thoroughly than jitter does. Tune on real hardware.
4. Reconstructed shader model
The formulas below are a reconstruction. They reproduce the observable effect and match the Android developer's description, but they are not Apple's implementation, which has not been published.
AGSL lets a RuntimeShader compute an output colour for every pixel inside the platform rendering pipeline, with values supplied from app code as uniforms. That is what makes a continuously updated sensor value usable as an animation driver.
4.1 Hinge-centred mask
d = |x - h|
M(x) = 1 - smoothstep(0, w, d)
w controls the width of the transition band. Near the hinge M approaches 1, away from it M falls to 0. Every later effect multiplies by this mask, which keeps distortion attached to the crease rather than washing over the panel.
4.2 Progressive blur
B(p) = B_max * sin(pi * p)
B(x,p) = M(x) * B_max * sin(pi * p)
Closed sharp, half open softest, fully open sharp again. For calibration, the Three.js study of the same effect uses a maximum blur radius of 72 source pixels and applies darkening at twice the blur intensity, clamped to black. A starting point, not a rule.
4.3 Dissolve and transparency
alpha(x,p) = 1 - k * M(x) * sin(pi * p)
With k = 0.65, untouched regions stay near alpha 1.00 while the fold band drops to about 0.35 at the midpoint. No transparent panel exists in the hardware. It works because the viewer has no independent evidence about what is behind the moving half.
4.4 Two-image interpolation
C(x,y,p) = mix( C_outer, C_inner, t )
t = 3p^2 - 2p^3
The mix factor need not equal hinge progress. Cubic smoothstep has zero derivative at both ends, removing the mechanical quality of a linear crossfade. The difference is small at the extremes and noticeable through the middle third.
4.5 Perspective compression
s(theta) = |cos(theta)|
x' = h + (x - h) * s(theta)
x'' = x' + k * sin(theta)
The first narrows content toward the hinge as the surface turns away. The second adds a lateral offset so content slides behind the fold rather than shrinking in place. The Three.js study solves the same problem differently, rotating only the cover half while the rear-camera half stays fixed and keeping a front-view projection of screen content during the fold.
4.6 Lighting and specular edge
L(x,p) = 1 - lambda * M(x) * sin(pi * p)
C_lit = C * L
H(x) = exp( -(x - h)^2 / (2 * sigma^2) )
C_final = C_lit + q * H(x)
Read from the outside in: normal surface, dark crease, thin bright line. This is the step most amateur recreations omit, and its absence is usually why a technically correct blur still looks flat.
4.7 Combined model
p = clamp(theta / 180, 0, 1)
M(x) = 1 - smoothstep(0, w, |x - h|)
B(x,p) = B_max * M(x) * sin(pi * p)
A(x,p) = 1 - k * M(x) * sin(pi * p)
T(p) = 3p^2 - 2p^3
C = (1 - T) * C_outer + T * C_inner
C_final = Blur( Transform(C, theta), B ) * A * L
5. Implementation
5.1 Reading the sensor in Kotlin
fun hingeProgress(context: Context, smoothing: Float = 0.25f): Flow<Float> = callbackFlow {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val hinge = manager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
if (hinge == null) { close(); return@callbackFlow }
// Do not hardcode 180. Ask the sensor what it reports.
val maxAngle = hinge.maximumRange.takeIf { it > 0f } ?: 180f
var filtered = Float.NaN
val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
val raw = (event.values[0] / maxAngle).coerceIn(0f, 1f)
filtered = if (filtered.isNaN()) raw else filtered + smoothing * (raw - filtered)
trySend(filtered)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
}
manager.registerListener(listener, hinge, SensorManager.SENSOR_DELAY_GAME)
awaitClose { manager.unregisterListener(listener) }
}
Two deliberate choices. SENSOR_DELAY_GAME rather than SENSOR_DELAY_FASTEST, because the faster rate costs power without a visibly better result at 120 Hz. And maximumRange rather than a literal 180, because not every hinge reports the same span.
5.2 The AGSL shader
uniform shader outerImage;
uniform shader innerImage;
uniform float2 size;
uniform float progress;
uniform float hingeX;
uniform float bandWidth;
uniform float maxBlur;
uniform float alphaK;
uniform float darkK;
uniform float gleamK;
float ease(float t) { return t * t * (3.0 - 2.0 * t); }
half4 blurSample(shader img, float2 uv, float radius) {
if (radius < 0.5) return img.eval(uv);
half4 sum = half4(0.0);
float total = 0.0;
for (int i = -4; i <= 4; i++) {
float o = float(i) / 4.0;
float wgt = exp(-o * o * 2.0);
sum += img.eval(uv + float2(o * radius, 0.0)) * half(wgt);
total += wgt;
}
return sum / half(total);
}
half4 main(float2 fragCoord) {
float2 uv = fragCoord;
float x = fragCoord.x / size.x;
float d = abs(x - hingeX);
float mask = 1.0 - smoothstep(0.0, bandWidth, d);
float env = sin(3.14159265 * progress);
float theta = progress * 3.14159265;
float s = abs(cos(theta * 0.5));
float xp = hingeX + (x - hingeX) * mix(1.0, s, mask);
float2 warped = float2(xp * size.x, uv.y);
float radius = maxBlur * mask * env;
half4 outer = blurSample(outerImage, warped, radius);
half4 inner = blurSample(innerImage, warped, radius);
half4 color = mix(outer, inner, half(ease(progress)));
float light = 1.0 - darkK * mask * env;
float sigma = bandWidth * 0.35;
float gleam = exp(-(d * d) / (2.0 * sigma * sigma)) * gleamK * env;
color.rgb = color.rgb * half(light) + half(gleam);
color.a = color.a * half(1.0 - alphaK * mask * env);
return color;
}
5.3 Driving it from Compose
@Composable
fun FoldSurface(outer: ImageBitmap, inner: ImageBitmap, progress: Float, modifier: Modifier = Modifier) {
val shader = remember { RuntimeShader(FOLD_AGSL) }
Box(
modifier
.fillMaxSize()
.onSizeChanged { shader.setFloatUniform("size", it.width.toFloat(), it.height.toFloat()) }
.graphicsLayer {
shader.setFloatUniform("progress", progress)
shader.setFloatUniform("hingeX", 0.5f)
shader.setFloatUniform("bandWidth", 0.18f)
shader.setFloatUniform("maxBlur", 72f)
shader.setFloatUniform("alphaK", 0.65f)
shader.setFloatUniform("darkK", 0.45f)
shader.setFloatUniform("gleamK", 0.12f)
shader.setInputShader("outerImage", ImageShader(outer))
shader.setInputShader("innerImage", ImageShader(inner))
renderEffect = RenderEffect
.createRuntimeShaderEffect(shader, "outerImage")
.asComposeRenderEffect()
clip = true
}
)
}
Uniform updates inside graphicsLayer are cheap; the block re-runs when progress changes and does not recompose the tree above it. Rebuilding the RuntimeShader every frame is the most common performance mistake in this kind of code, which is why it sits behind remember.
5.4 Second display with Presentation
class FoldPresentation(context: Context, display: Display, private val progress: StateFlow<Float>)
: Presentation(context, display) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ComposeView(context).apply {
setContent {
val p by progress.collectAsState()
FoldSurface(outer = coverCapture, inner = innerCapture, progress = p)
}
})
}
}
val dm = getSystemService(DisplayManager::class.java)
dm.displays
.firstOrNull { it.displayId != windowManager.defaultDisplay.displayId }
?.let { FoldPresentation(this, it, progressFlow).show() }
Both surfaces read the same StateFlow, so exactly one progress value exists in the system. Two independently animated screens drift apart within a few frames, and the drift is obvious edge-on.
5.5 macOS and web equivalents
The structure transfers. macTilt polls the lid angle sensor at 60 Hz, captures the desktop with ScreenCaptureKit and renders the fold with Metal Shading Language, with a SwiftUI panel for trigger angles. On the web there is no hinge, so the Three.js study substitutes a slider for the sensor and keeps everything downstream identical. Only the first stage of the pipeline is platform specific.
6. Performance notes
- Only shade the band. Outside the mask, return the source sample directly.
- Downsample before blurring. Half resolution is indistinguishable at these radii and roughly quarters the work.
- Stop when the hinge stops. If p has not moved by more than a small epsilon, skip the frame.
- Cache the captures. Re-capturing every frame defeats the point; capture once when movement begins.
7. Limitations of the recreation
Coverage frequently described the demo as Apple's feature arriving on Samsung hardware. That is inaccurate. It is an app animating two captured images with a blur effect, and as reported, it cannot be built into SystemUI in that form. Independent coverage made the same point.
The gap between demo and platform feature is not polish. A system-wide version requires every application window to hand its content to the same compositing stage at the same progress value, which is access only the platform owner can grant itself. That is why the effect shipped first as an Apple feature, and why the honest description of the recreation is that it reproduces the appearance, not the integration.
8. The viewing angle problem
The developer noted the illusion is most convincing viewed straight on, and that the novelty gives way to mild irritation with repeated use. Both point at the same structural gap. The shader knows the hinge angle. It does not know where the viewer's eyes are.
Image = f( theta_hinge, theta_viewer, x, y )
At theta_viewer = 0 the simulated and physical perspectives agree. Off axis they diverge, because the shader keeps drawing a fold seen head on while the eye sees the panel obliquely. This is a property of the approach, not a bug in any implementation. Closing it means face tracking and a per-viewer projection, difficult to justify for a wallpaper transition.
9. Conclusion
Apple's fold animation is not impressive because blur is hard. It is impressive because software rendering is synchronised with physical motion closely enough that the brain treats both halves of the display as one continuous surface. The sensor is the clock, the shader is the renderer, and the illusion lives or dies on how tightly those two agree.
10. References
- Apple Newsroom, Apple unveils iPhone Duo, 9 September 2026.
- r/GalaxyFold, Tried to recreate the iPhone Duo animation on my Fold, 9 September 2026.
- lqSky7, iphone-duo-macos-animation (macTilt), GitHub.
- chuspeeism, iphone-duo, GitHub, and the live build at iphone-duo-tawny.vercel.app.
- 9to5Google, Someone recreated the iPhone Duo's mesmerizing open animation on the Galaxy Z Fold 8, 10 September 2026.
- Sportskeeda Tech, iPhone Duo cover animation could be recreated on the Samsung Galaxy Z Fold 8, but there are limitations.
- Android Developers, Sensor reference, TYPE_HINGE_ANGLE, API level 30.
- Android Developers, AGSL, Android Graphics Shading Language.
- Android Developers, Presentation reference.
- Business Today, The Mac Observer, Sammy Fans, Smartprix, TidBITS and Engadget, September 2026 coverage.