Animation de pliage de l'iPhone Duo : capteur de charnière, shader AGSL et recréation sur Galaxy Z Fold 8
L'animation de pliage de l'iPhone Duo est une transition d'interface du premier téléphone pliable d'Apple, présenté le 9 septembre 2026 et commercialisé à partir du 23 octobre 2026. À l'ouverture comme à la fermeture, le fond d'écran et l'interface se floutent, s'assombrissent et semblent traverser la moitié mobile de l'écran avant de redevenir nets. La transition n'avance pas sur une horloge mais sur l'angle mesuré de la charnière. C'est pour cela qu'elle suit la main plutôt qu'un minuteur.
Un jour après l'annonce, un développeur a publié une recréation fonctionnelle sur un Samsung Galaxy Z Fold 8 : une application Android autonome qui lit le capteur de charnière, affiche sur les deux écrans via l'API Presentation et pilote un shader AGSL avec la valeur courante de la charnière. La démonstration utilise des captures des écrans d'accueil interne et externe plutôt que le launcher réel, limite que le développeur a signalée lui-même.
1. Contexte matériel
L'iPhone Duo associe une dalle interne Super Retina XDR de 7,6 pouces à finition nano-texturée et une dalle externe de 5,4 pouces couvrant environ quatre-vingt-dix pour cent de la surface d'écran d'un iPhone 18 Pro. Les deux offrent ProMotion et Always On jusqu'à 3 000 nits. La puce A20 Pro compte six cœurs CPU, sept cœurs GPU et un double Neural Engine 16 cœurs, refroidis par chambre à vapeur, avec jusqu'à trente-cinq pour cent de performances soutenues en plus par rapport à l'iPhone 17 Pro selon Apple. La charnière compte plus de cent composants, chaque moitié possède sa batterie. Prix à partir de 1 999 dollars, précommandes le 16 octobre 2026, iOS 27.1.
Deux points comptent pour l'animation : la réserve de puissance GPU, car un flou par pixel sur une dalle de 7,6 pouces à 120 Hz coûte cher sur la durée d'un mouvement, et la charnière elle-même, car une transition censée suivre la géométrie n'est crédible qu'avec un angle lisse et peu latent.
2. Comportement observable
- Flou progressif concentré près du pli, maximal à mi-course.
- Assombrissement de la même bande, lu comme une ombre dans le pli.
- Transparence partielle de la moitié mobile.
- Compression perspective vers la charnière pendant la rotation.
- Fondu entre le contenu fermé et le contenu ouvert.
La propriété essentielle est la réversibilité. S'arrêter à mi-course fige l'effet dans l'état correspondant, inverser le geste inverse l'effet. Une animation temporelle ne peut pas faire cela, sa variable de progression n'ayant aucun lien avec la position physique.
3. L'angle comme signal de commande
3.1 Disponibilité
Android expose un capteur de charnière depuis le niveau d'API 30. Sensor.TYPE_HINGE_ANGLE renvoie l'angle entre les deux moitiés en degrés. La plage varie selon les appareils, donc le code de production la lit sur l'objet capteur au lieu de supposer 0 à 180.
Sous macOS, l'équivalent est le capteur d'angle d'écran interne, accessible comme IOHIDDevice de vendeur 0x05AC et produit 0x8104. Le projet macTilt l'interroge à 60 Hz avec des angles de début et de fin configurables, par exemple 80 degrés et 3 degrés. Apple ne documente pas ce périphérique.
3.2 Normalisation
p = clamp( (theta - theta_closed) / (theta_open - theta_closed), 0, 1 )
p_smooth = p_smooth + a * (p_raw - p_smooth), a environ 0.25
Trop de lissage et l'effet traîne derrière la main, ce qui détruit l'illusion plus sûrement que le bruit.
4. Modèle de shader reconstruit
Les formules qui suivent sont une reconstruction. Elles reproduisent l'effet visible et correspondent à la description du développeur Android, mais ce n'est pas l'implémentation d'Apple, qui n'est pas publiée.
d = |x - h|
M(x) = 1 - smoothstep(0, w, d)
B(x,p) = M(x) * B_max * sin(pi * p)
alpha = 1 - k * M(x) * sin(pi * p)
t = 3p^2 - 2p^3
C = mix( C_outer, C_inner, t )
s(th) = |cos(th)|
x' = h + (x - h) * s(th)
L(x,p) = 1 - lambda * M(x) * sin(pi * p)
H(x) = exp( -(x - h)^2 / (2 * sigma^2) )
C_final = Blur( Transform(C, th), B ) * alpha * L + q * H(x)
Pour calibrer, l'étude Three.js du même effet utilise un rayon de flou maximal de 72 pixels source et un assombrissement d'intensité double, borné au noir.
La lecture de l'extérieur vers l'intérieur donne surface normale, pli sombre, fine ligne brillante. C'est l'étape que la plupart des recréations amateurs oublient, et c'est souvent pourquoi un flou techniquement correct paraît plat.
5. Implémentation
5.1 Lecture du capteur en 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 }
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) }
}
5.2 Le shader AGSL
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) {
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, fragCoord.y);
float radius = maxBlur * mask * env;
half4 color = mix(blurSample(outerImage, warped, radius),
blurSample(innerImage, warped, radius),
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 Pilotage depuis Compose et second écran
val shader = remember { RuntimeShader(FOLD_AGSL) } // jamais recréé par frame
val dm = getSystemService(DisplayManager::class.java)
dm.displays
.firstOrNull { it.displayId != windowManager.defaultDisplay.displayId }
?.let { FoldPresentation(this, it, progressFlow).show() }
Les deux surfaces lisent le même StateFlow. Deux écrans animés indépendamment dérivent en quelques images, et cela se voit immédiatement de profil.
5.4 Équivalents macOS et web
La structure se transpose. macTilt interroge le capteur d'angle à 60 Hz, capture le bureau avec ScreenCaptureKit et rend le pli en Metal Shading Language. Sur le web il n'y a pas de charnière, donc l'étude Three.js remplace le capteur par un curseur et garde le reste identique. Seule la première étape du pipeline dépend de la plateforme.
6. Performances
- Ne shader que la bande, renvoyer directement le pixel source ailleurs.
- Sous-échantillonner avant le flou, la demi-résolution est indiscernable à ces rayons.
- Sauter les images quand p n'a pratiquement pas bougé.
- Capturer les écrans une fois au début du mouvement, pas à chaque image.
7. Limites de la recréation
Beaucoup d'articles ont présenté la démonstration comme la fonction d'Apple portée sur Samsung. C'est inexact. Il s'agit d'une application qui anime deux captures avec un flou, et sous cette forme elle ne peut pas être intégrée à la SystemUI. Une version système exigerait que chaque fenêtre transmette son contenu au même étage de composition, un accès que seul le propriétaire de la plateforme peut s'accorder.
8. Le problème de l'angle de vue
Image = f( theta_hinge, theta_viewer, x, y )
Le shader connaît l'angle de la charnière, pas la position des yeux. De face, les perspectives simulée et réelle coïncident. De côté, elles divergent. C'est une propriété de l'approche, pas un défaut d'implémentation. La corriger demanderait un suivi du visage et une projection par observateur.
9. Conclusion
L'animation d'Apple n'impressionne pas parce que le flou serait difficile. Elle impressionne parce que le rendu logiciel est synchronisé avec le mouvement physique au point que le cerveau traite les deux moitiés de l'écran comme une seule surface continue. Le capteur est l'horloge, le shader est le moteur de rendu, et l'illusion dépend entièrement de leur accord.
10. Références
- Apple Newsroom, Apple unveils iPhone Duo, 9 septembre 2026.
- r/GalaxyFold, recréation de l'animation iPhone Duo, 9 septembre 2026.
- lqSky7, iphone-duo-macos-animation (macTilt), GitHub.
- chuspeeism, iphone-duo, GitHub.
- 9to5Google, 10 septembre 2026.
- Android Developers : Sensor, AGSL, Presentation.