All Articles
8 minUpdated

Animación de plegado del iPhone Duo: sensor de bisagra, shader AGSL y la recreación en Galaxy Z Fold 8

Most RecentTrendingAndroid

La animación de plegado del iPhone Duo es una transición de interfaz del primer teléfono plegable de Apple, presentado el 9 de septiembre de 2026 y a la venta desde el 23 de octubre de 2026. Al abrir o cerrar el aparato, el fondo de pantalla y la interfaz se desenfocan, se oscurecen y parecen atravesar la mitad móvil de la pantalla antes de volver a enfocarse. La transición no avanza contra un reloj, sino contra el ángulo medido de la bisagra. Por eso sigue a la mano y no a un temporizador.

Un día después del anuncio, un desarrollador publicó una recreación funcional en un Samsung Galaxy Z Fold 8: una app Android independiente que lee el sensor de bisagra, dibuja en ambas pantallas mediante la API Presentation y alimenta un shader AGSL con el valor actual de la bisagra. La demo usa capturas de las pantallas de inicio interna y externa en lugar del launcher real, una limitación que el propio desarrollador señaló.

Figura. La transición de plegado grabada en el dispositivo, 720 por 1280 a 30 fps. Fíjese en la banda del pliegue: el desenfoque alcanza su máximo cerca del punto medio y desaparece en ambas posiciones de reposo, la envolvente sin(πp) de la sección 4.

1. Contexto de hardware

El iPhone Duo combina un panel interno Super Retina XDR de 7,6 pulgadas con acabado nano-texturizado y un panel externo de 5,4 pulgadas que cubre en torno al noventa por ciento del área de pantalla de un iPhone 18 Pro. Ambos ofrecen ProMotion y Always On con hasta 3.000 nits. El chip A20 Pro tiene seis núcleos de CPU, siete de GPU y un Neural Engine doble de 16 núcleos, refrigerado por cámara de vapor, con hasta un treinta y cinco por ciento más de rendimiento sostenido que el iPhone 17 Pro según Apple. La bisagra usa más de cien componentes y cada mitad lleva su propia batería. Desde 1.999 dólares, reservas el 16 de octubre de 2026, iOS 27.1.

Dos detalles importan para la animación: margen sostenido de GPU, porque un desenfoque por píxel sobre un panel de 7,6 pulgadas a 120 Hz es caro durante todo un movimiento, y la propia bisagra, porque una transición que dice seguir la geometría solo convence con un ángulo suave y de baja latencia.

2. Comportamiento observable

  1. Desenfoque progresivo concentrado junto al pliegue, máximo a media apertura.
  2. Oscurecimiento de la misma banda, que se lee como sombra en el pliegue.
  3. Transparencia parcial de la mitad en movimiento.
  4. Compresión en perspectiva hacia la bisagra mientras el panel gira.
  5. Fundido entre el contenido cerrado y el abierto.

Lo esencial es la reversibilidad. Detener el movimiento congela el efecto en el estado correspondiente; invertirlo lo invierte. Una animación temporal no puede hacerlo, porque su variable de progreso no guarda relación con la posición física del aparato.

3. El ángulo como señal de control

Android expone un sensor de bisagra desde el nivel de API 30. Sensor.TYPE_HINGE_ANGLE devuelve el ángulo entre las dos mitades en grados. El rango varía entre dispositivos, así que el código de producción lo lee del objeto sensor en vez de suponer 0 a 180.

En macOS el equivalente es el sensor interno de ángulo de tapa, accesible como IOHIDDevice con vendor 0x05AC y product 0x8104. El proyecto macTilt lo consulta a 60 Hz con ángulos de inicio y fin configurables, por ejemplo 80 y 3 grados. Apple no documenta ese dispositivo.

p = clamp( (theta - theta_closed) / (theta_open - theta_closed), 0, 1 )
p_smooth = p_smooth + a * (p_raw - p_smooth),   a cercano a 0.25

Demasiado suavizado y el efecto se retrasa respecto a la mano, lo cual arruina la ilusión más que el ruido.

4. Modelo de shader reconstruido

Las fórmulas siguientes son una reconstrucción. Reproducen el efecto visible y coinciden con la descripción del desarrollador de Android, pero no son la implementación de Apple, que no se ha publicado.

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)

Como calibración, el estudio en Three.js del mismo efecto usa un radio máximo de desenfoque de 72 píxeles de origen y oscurece al doble de intensidad, limitado a negro. Con k = 0,65 la zona intacta se queda cerca de alfa 1,00 y la banda del pliegue baja a unos 0,35.

Leído de fuera hacia dentro queda superficie normal, pliegue oscuro, línea brillante estrecha. Ese es el paso que casi todas las recreaciones caseras omiten, y suele ser la razón por la que un desenfoque técnicamente correcto se ve plano.

5. Implementación

5.1 Lectura del sensor 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 El 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 Compose y segunda pantalla

val shader = remember { RuntimeShader(FOLD_AGSL) }   // nunca recrear por frame

val dm = getSystemService(DisplayManager::class.java)
dm.displays
  .firstOrNull { it.displayId != windowManager.defaultDisplay.displayId }
  ?.let { FoldPresentation(this, it, progressFlow).show() }

Ambas superficies leen el mismo StateFlow, así que existe un único valor de progreso en el sistema. Dos pantallas animadas por separado se desincronizan en pocos fotogramas y se nota mirando el borde.

5.4 Equivalentes en macOS y web

La estructura se traslada. macTilt consulta el sensor de ángulo a 60 Hz, captura el escritorio con ScreenCaptureKit y dibuja el pliegue con Metal Shading Language. En la web no hay bisagra, así que el estudio en Three.js sustituye el sensor por un control deslizante y mantiene idéntico todo lo demás. Solo la primera etapa del pipeline depende de la plataforma.

6. Rendimiento

  • Sombrear solo la banda y devolver el píxel de origen fuera de la máscara.
  • Reducir resolución antes del desenfoque; a estos radios la mitad es indistinguible.
  • Saltar fotogramas cuando p apenas ha cambiado.
  • Capturar las pantallas una vez al iniciar el movimiento, no en cada fotograma.

7. Limitaciones de la recreación

Mucha cobertura describió la demo como la función de Apple llegando a Samsung. No es exacto. Es una app que anima dos capturas con un desenfoque y, en esa forma, no puede integrarse en la SystemUI. Una versión de sistema exigiría que cada ventana entregue su contenido a la misma etapa de composición, un acceso que solo el dueño de la plataforma puede concederse.

8. El problema del ángulo de visión

Image = f( theta_hinge, theta_viewer, x, y )

El shader conoce el ángulo de la bisagra, no dónde están los ojos. De frente coinciden la perspectiva simulada y la real; de lado divergen. Es una propiedad del enfoque, no un fallo de implementación. Corregirlo exigiría seguimiento facial y proyección por espectador.

9. Conclusión

La animación de Apple no impresiona porque el desenfoque sea difícil. Impresiona porque el renderizado va sincronizado con el movimiento físico lo bastante bien como para que el cerebro trate ambas mitades como una sola superficie continua. El sensor es el reloj, el shader es el renderizador, y la ilusión depende de cuánto coincidan.

10. Referencias

  1. Apple Newsroom, Apple unveils iPhone Duo, 9 de septiembre de 2026.
  2. r/GalaxyFold, recreación de la animación del iPhone Duo, 9 de septiembre de 2026.
  3. lqSky7, iphone-duo-macos-animation (macTilt), GitHub.
  4. chuspeeism, iphone-duo, GitHub.
  5. 9to5Google, 10 de septiembre de 2026.
  6. Android Developers: Sensor, AGSL, Presentation.