How the iPhone Duo Fold Animation Works
iPhone Duo fold animation
From the engineering notebook of Zubair Hussain Shah. An article on sensor-driven interface rendering on foldable devices.
Not to be confused with the general fold-aware layout APIs in Jetpack WindowManager, which describe window geometry rather than animation.
The iPhone Duo fold animation is a user interface transition on Apple's iPhone Duo, the company's first foldable handset, announced on 9 September 2026 and shipping from 23 October 2026.[1] As the device is opened or closed, the wallpaper and interface progressively blur, darken and appear to pass through the moving half of the display before resolving back into focus. Unlike a conventional animation, which advances against a clock, the transition advances against the measured physical angle of the hinge, so it tracks the hand rather than 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 application that reads the hinge angle sensor, renders on both displays through the Presentation API and drives an AGSL runtime shader from the current hinge value.[2][5] The demonstration uses captured screenshots of the inner and outer home screens rather than the live launcher, a limitation the developer acknowledged and which press coverage repeated.[5][6]
This article describes the observable behaviour, a reconstruction of the mathematics that reproduces it, and working implementation patterns on Android, macOS and the web. No part of Apple's implementation is public, and nothing below should be read as a description of Apple's own shader code.
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, with peak brightness quoted at 3,000 nits. The device uses the A20 Pro, a six-core CPU with a seven-core GPU and a dual 16-core Neural Engine, cooled by a vapour chamber, and Apple claims up to thirty-five percent better sustained performance than the iPhone 17 Pro. A hinge assembled from more than one hundred components supports the inner display, and the two halves carry separate batteries. Pricing starts at $1,999, with pre-orders from 16 October 2026.[1]
Two hardware details matter for the animation specifically. The first is sustained GPU headroom, because a per-pixel blur across a 7.6-inch panel at 120 Hz is not a cheap effect to run for the length of a hinge movement. The second is the hinge itself. A transition that claims to track physical geometry is only convincing if the angle reported by the hardware is smooth and low-latency; a noisy or coarsely quantised sensor produces visible stepping that no amount of shader work will hide.
| Specification | iPhone Duo |
|---|---|
| Inner display | 7.6 in Super Retina XDR, nano-texture, ProMotion, Always On |
| Outer display | 5.4 in Super Retina XDR, ProMotion, Always On |
| Peak brightness | 3,000 nits |
| Chip | A20 Pro, 6-core CPU, 7-core GPU, dual 16-core Neural Engine |
| Rear cameras | 48 MP Fusion Main with 2x optical telephoto, 48 MP Ultra Wide |
| Battery life | Up to 31 h video on inner display, 44 h on outer, 24 h mixed |
| Charging | 50 percent in about 20 min wired, about 30 min wireless |
| Software | iOS 27.1 at launch |
| Price | From $1,999, 256 GB to 2 TB |
2. Observable behaviour
From hands-on coverage and the launch material, the transition can be decomposed into five simultaneous effects. None of them is individually novel. The combination, and the fact that all five are parameterised by one continuous physical value, is what produces the impression of a single bending surface.
- Progressive blur concentrated near the fold, at its strongest around the midpoint of the movement.
- Darkening across the same band, which reads as a shadow cast into the crease.
- Partial transparency, so that the moving half appears to be something you are looking through rather than at.
- Perspective compression, with content narrowing toward the hinge as the panel rotates away from the viewer.
- A cross-dissolve between the content appropriate to the closed state and the content appropriate to the open state.
The crucial property is reversibility. Stopping the movement partway leaves the effect frozen at the matching intermediate state, and reversing direction reverses the effect. A time-based animation cannot do this without additional machinery, 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. The constant Sensor.TYPE_HINGE_ANGLE reports the angle between the two halves of a foldable device in degrees, delivered as an ordinary sensor event stream.[7] Range and rest values differ between devices, which is why production code should read them from the sensor object rather than assuming zero to one hundred and eighty.
On macOS the equivalent signal is the internal lid angle sensor, reachable as an IOHIDDevice with vendor identifier 0x05AC and product identifier 0x8104. The macTilt project polls it at 60 Hz and exposes configurable start and end angles, for example beginning the fold effect at 80 degrees and completing it at 3 degrees.[3] Apple does not document this device publicly, so the approach is reverse-engineered rather than supported.
3.2 Normalisation
Whatever the source, the raw angle is converted to a bounded progress value before it reaches any rendering code:
p = clamp( (θ − θclosed) / (θopen − θclosed), 0, 1 )giving p = 0 when closed, p = 0.5 at the halfway point and p = 1 when fully open. Keeping normalisation in one place has a practical benefit beyond tidiness: the shader then contains no device-specific constants, and the same shader runs unchanged on hardware whose hinge reports a different range.
A detail easy to miss is smoothing. Raw hinge readings jitter, and a jittering uniform produces a visible shimmer in the blur band. A small exponential filter is usually enough:
p_smooth ← p_smooth + α · (p_raw − p_smooth), α ≈ 0.25
Too much smoothing and the effect lags behind the hand, which destroys the illusion more thoroughly than jitter does. The value of α is worth tuning on real hardware rather than guessing.
4. Reconstructed shader model
Android's AGSL, the Android Graphics Shading Language, lets a RuntimeShader compute an output colour for every pixel inside the platform's rendering pipeline, with values supplied from application code as uniforms.[8] That is the mechanism that makes a continuously updated sensor value usable as an animation driver.
4.1 Hinge-centred mask
Place the hinge at normalised horizontal coordinate h, typically 0.5. For a pixel at normalised horizontal position x:
d = |x − h| M(x) = 1 − smoothstep(0, w, d)The parameter w controls the width of the transition band. Near the hinge M approaches 1; away from it M falls to 0. Every subsequent effect is multiplied by this mask, which is what keeps the distortion attached to the crease instead of washing over the whole panel.
4.2 Progressive blur
Blur strength is a function of progress, not a constant:
B(p) = Bmax · sin(π p)which yields B(0) = 0, B(0.5) = Bmax and B(1) = 0. Closed is sharp, half open is softest, fully open is sharp again. Combined with the mask:
B(x, p) = M(x) · Bmax · sin(π p)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.[4] That ratio is a useful starting point rather than a rule.
4.3 Dissolve and transparency
Part of the moving panel reads as translucent. Modelling opacity as
α(x, p) = 1 − k · M(x) · sin(π p)with k = 0.65 gives roughly alpha 1.00 in untouched regions and alpha 0.35 inside the fold band at the midpoint. No transparent panel exists in the hardware. The effect is entirely a rendering illusion, and it works because the viewer has no independent evidence about what is behind the moving half.
4.4 Two-image interpolation
The Android recreation crossfades between a capture of the outer display and a capture of the inner one:[2]
C(x, y, p) = mix( Couter(x, y), Cinner(x, y), t )The mix factor need not equal hinge progress. Passing it through cubic smoothstep
t = S(p) = 3p² − 2p³gives zero derivative at both ends, which removes the mechanical quality of a linear crossfade. Perceptually the difference is small at the extremes and noticeable in the middle third of the movement.
4.5 Perspective compression
Blur alone reads as a filter applied on top of the interface. For the content to read as attached to a rotating surface, its geometry has to change too. A simplified projection term:
s(θ) = |cos θ| x′ = h + (x − h) · s(θ) x″ = x′ + k · sin θThe first expression narrows the content toward the hinge as the surface turns away from the viewer. The second adds a lateral offset so that content appears to slide behind the fold rather than shrink in place. The Three.js study handles the same problem differently, rotating only the cover half while holding the rear-camera half fixed and keeping a front-view projection of the screen content during the fold.[4]
4.6 Lighting and specular edge
A real folded surface is not uniformly lit. Darkening the fold band and then laying a narrow highlight over it approximates curved glass:
L(x, p) = 1 − λ · M(x) · sin(π p) Clit = C · L H(x) = exp( −(x − h)² / 2σ² ) Cfinal = Clit + q · H(x)The result reads from the outside in as 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.
Combined model
p = clamp(θ / 180, 0, 1)
M(x) = 1 − smoothstep(0, w, |x − h|)
B(x,p) = B_max · M(x) · sin(π p)
A(x,p) = 1 − k · M(x) · sin(π p)
T(p) = 3p² − 2p³
C = (1 − T) · C_outer + T · C_inner
C_final = Blur( Transform(C, θ), B ) · A · L
5. Implementation
The following is a minimal but complete path from sensor to screen on Android 13 or newer. It is written to be readable rather than to be dropped into production unchanged.
5.1 Reading the sensor in Kotlin
HingeProgress.kt
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
/**
* Emits normalised hinge progress in 0..1.
* 0 = closed, 1 = fully open. Emits nothing if the device has no hinge.
*/
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 0..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 things are deliberate here. SENSOR_DELAY_GAME rather than SENSOR_DELAY_FASTEST, because the faster rate costs power without producing 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
AGSL is close enough to GLSL fragment shading that the structure will look familiar, with the important difference that the entry point receives a coordinate and returns a colour, and inputs arrive as uniform declarations.[8]
fold.agsl
uniform shader outerImage; // capture of the cover screen
uniform shader innerImage; // capture of the inner screen
uniform float2 size; // surface size in pixels
uniform float progress; // p, normalised hinge 0..1
uniform float hingeX; // h, normalised 0..1, usually 0.5
uniform float bandWidth; // w
uniform float maxBlur; // B_max, in pixels
uniform float alphaK; // k
uniform float darkK; // lambda
uniform float gleamK; // q
// cubic smoothstep easing, zero velocity at both ends
float ease(float t) { return t * t * (3.0 - 2.0 * t); }
// 9-tap separable-ish sample. Cheap, and good enough at these radii.
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;
// 1. hinge-centred mask
float d = abs(x - hingeX);
float mask = 1.0 - smoothstep(0.0, bandWidth, d);
// 2. fold envelope, peaks at p = 0.5
float env = sin(3.14159265 * progress);
// 3. perspective compression toward the hinge
float theta = progress * 3.14159265; // 0..pi across the movement
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);
// 4. blur, strongest in the band at mid fold
float radius = maxBlur * mask * env;
half4 outer = blurSample(outerImage, warped, radius);
half4 inner = blurSample(innerImage, warped, radius);
// 5. cross-dissolve with eased factor
half4 color = mix(outer, inner, half(ease(progress)));
// 6. lighting: darken the crease, then add a narrow gleam
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);
// 7. partial transparency across the fold band
color.a = color.a * half(1.0 - alphaK * mask * env);
return color;
}
5.3 Driving it from Compose
FoldSurface.kt
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@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) // matches the Three.js study
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
}
)
}
// collection site
val progress by hingeProgress(context).collectAsState(initial = 0f)
FoldSurface(outer = coverCapture, inner = innerCapture, progress = progress)
Uniform updates inside graphicsLayer are cheap; the block re-runs when progress changes and does not recompose the tree above it. Rebuilding the RuntimeShader on every frame, by contrast, is the single most common performance mistake in this kind of code, which is why it sits behind remember.
5.4 Second display with Presentation
Android's Presentation class attaches content to a secondary display, and recent releases extend it to eligible built-in displays, which is what makes a foldable's cover panel addressable from an application.[9]
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)
}
})
}
}
// pick the display that is not the one we are already on
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 there is exactly one progress value in the system. Two independently animated screens would drift apart within a few frames, and the drift is obvious to anyone looking at the device edge-on.
5.5 macOS and web equivalents
The same structure transfers. On macOS, 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 control panel for the trigger angles.[3] On the web there is no hinge to read, so the browser-based Three.js study substitutes a slider or a playback control for the sensor and keeps everything downstream of that identical.[4] The lesson is that only the first stage of the pipeline is platform-specific.
| Stage | Android | macOS | Web |
|---|---|---|---|
| Angle source | TYPE_HINGE_ANGLE | IOHIDDevice 0x05AC / 0x8104 | slider or scroll position |
| Content capture | screenshots or window content | ScreenCaptureKit | DOM or texture |
| Shading | AGSL RuntimeShader | Metal Shading Language | GLSL through Three.js |
| Second surface | Presentation | per-screen NSWindow | second canvas |
| Update rate | sensor events, ~60 to 120 Hz | 60 Hz polling | animation frame |
6. Performance notes
A naive implementation samples the source image nine times per pixel per frame for a full-screen blur, on both displays, for as long as the hinge is moving. On a large inner panel that is a considerable amount of texture bandwidth. Several mitigations are standard.
- Only shade the band. Outside the mask the shader can return the source sample directly, which the early return in the code above already does when the radius falls below half a pixel.
- Downsample before blurring. Blurring a half-resolution copy and scaling back up is visually indistinguishable at these radii and roughly quarters the work.
- Stop when the hinge stops. If p has not changed by more than a small epsilon, skip the frame entirely rather than redrawing an identical image.
- Cache the captures. Re-capturing the inner and outer content on every frame defeats the point; capture once when the movement begins.
7. Limitations of the recreation
Coverage of the Galaxy Z Fold 8 demonstration frequently described it as Apple's feature arriving on Samsung hardware. That framing is inaccurate. The demonstration is an application that animates two captured images with a blur effect, and as reported, it cannot be built into SystemUI in that form.[5] Independent coverage made the same point about it being a demo rather than a system-wide capability.[6]
The gap between demo and platform feature is not a matter of 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 the reason the effect shipped first as an Apple feature rather than as an Android application, and it is also why the honest description of the recreation is that it reproduces the appearance, not the integration.
8. The viewing-angle problem
The developer noted that the illusion is most convincing viewed straight on, and that the novelty gives way to mild irritation with repeated use. Both observations point at the same structural gap in the model. The shader knows the hinge angle. It does not know where the viewer's eyes are.
Image = f( θhinge, θviewer, x, y )At θviewer = 0 the simulated perspective and the physical perspective agree and the effect holds. Off-axis they diverge, because the shader continues to draw a fold as seen head-on while the eye sees the panel obliquely. This is why a mathematically sound effect can look subtly wrong from the side, and it is a property of the approach rather than a bug in any particular implementation.
Whether that cost is ever worth paying is a product question rather than a technical one. Head-tracked rendering exists and is well understood; the objection is power draw and the awkwardness of a wallpaper effect that needs the front camera running.
9. Video
The clip below shows the transition as it actually runs. The original Galaxy Z Fold 8 demonstration is hosted on Reddit by its creator and is not reproduced here; the links point to the source instead.
- Original demonstration and developer commentary, r/GalaxyFold
- Video embedded in 9to5Google's report
- macTilt demonstration video
- Interactive Three.js version, runs in the browser
The clip is portrait because the effect is best judged at the device's own proportions. On a wide screen a full-width version of a 9:16 recording is mostly empty space on either side, so the player above is capped at roughly phone width and centred. If you later record a landscape version, add the class wide to the figure and the same markup switches to 16:9.
10. See also
- AGSL, Android Graphics Shading Language
- Learn about foldables, Android developer guide
- Jetpack WindowManager, fold-aware layout
- Zubair Hussain Shah, engineering portfolio
- More articles on rendering and mobile engineering
- Source code and experiments on GitHub
11. References
- "Apple unveils iPhone Duo". Apple Newsroom. 9 September 2026. Retrieved 11 September 2026.
- "Tried to recreate the iPhone Duo animation on my Fold". r/GalaxyFold. 9 September 2026. Includes the developer's own description of the sensor, Presentation and AGSL approach.
- lqSky7, "iphone-duo-macos-animation (macTilt)". GitHub. Swift, Metal Shading Language, ScreenCaptureKit, 60 Hz lid angle polling.
- chuspeeism, "iphone-duo". GitHub. Browser study of foldable screen transitions using Three.js, 72-pixel maximum blur radius. Live build at iphone-duo-tawny.vercel.app.
- "Someone recreated the iPhone Duo's mesmerizing open animation on the Galaxy Z Fold 8". 9to5Google. 10 September 2026.
- "iPhone Duo cover animation could be recreated on the Samsung Galaxy Z Fold 8, but there are limitations". Sportskeeda Tech. September 2026.
- "Sensor: TYPE_HINGE_ANGLE". Android Developers reference. Available since API level 30.
- "AGSL, Android Graphics Shading Language". Android Developers. Per-pixel colour computation via
RuntimeShader, with uniforms supplied from application code. - "Presentation". Android Developers reference. Placing content on a secondary display.
- "iPhone Duo vs Galaxy Z Fold 8: User recreates Apple's unfolding animation on Samsung foldable". Business Today. 11 September 2026.
- "Reddit User Recreates iPhone Duo Fold Animation on Galaxy Z Fold 8". The Mac Observer. September 2026.
- "Someone brought iPhone Duo animations to the Galaxy Z Fold 8". Sammy Fans. 10 September 2026.
- "This Open-Source Mac App Brings the iPhone Duo's Fold Animation to MacBooks". Smartprix. September 2026.
- "Apple Unfolds the iPhone Duo". TidBITS. 11 September 2026.
- "Everything Apple announced at the foldable iPhone Duo launch". Engadget. September 2026.
12. External links
- Official iPhone Duo announcement
- Android Sensor reference, hinge angle constant
- Interactive fold study, runs in any browser
- macTilt source, Swift and Metal
- Three.js fold study source

コメント
コメントを投稿