All Articles
4 minUpdated

iPhone Duo の折りたたみアニメーション:ヒンジ角センサー、AGSL シェーダー、Galaxy Z Fold 8 での再現

Most RecentTrendingAndroid

iPhone Duo の折りたたみアニメーションは、2026 年 9 月 9 日に発表され 10 月 23 日に発売される Apple 初の折りたたみ端末における UI トランジションです。端末を開閉すると、壁紙と UI が次第にぼけ、暗くなり、動いている側のディスプレイを通り抜けるように見えてから再び鮮明になります。このトランジションは時計ではなく、実測されたヒンジ角に沿って進みます。だからタイマーではなく手の動きに追従します。

発表の翌日、ある開発者が Samsung Galaxy Z Fold 8 上で動く再現版を公開しました。ヒンジセンサーを読み、Presentation API で両方のディスプレイに描画し、現在のヒンジ値で AGSL ランタイムシェーダーを駆動する単体アプリです。デモでは実際のランチャーではなく、内側と外側のホーム画面のスクリーンショットを使っています。開発者自身がその制約を明言しています。

図. 実機で撮影した折りたたみトランジション。720 x 1280、30 fps。折り目の帯に注目してください。ぼかしは中間付近で最大になり、両端の静止位置で消えます。第 4.2 節の sin(πp) の包絡線です。

1. ハードウェアの前提

iPhone Duo は、ナノテクスチャ仕上げの 7.6 インチ内側 Super Retina XDR パネルと、iPhone 18 Pro の画面面積の約 9 割に相当する 5.4 インチ外側パネルを組み合わせます。どちらも ProMotion と常時表示に対応し、ピーク輝度は 3,000 ニトです。チップは A20 Pro で、6 コア CPU、7 コア GPU、16 コアのデュアル Neural Engine をベイパーチャンバーで冷却し、Apple は iPhone 17 Pro 比で持続性能が最大 35 パーセント向上したとしています。ヒンジは 100 点を超える部品で構成され、各半分に独立したバッテリーがあります。価格は 1,999 ドルから、予約は 2026 年 10 月 16 日、ソフトウェアは iOS 27.1 です。

アニメーションにとって重要な点は 2 つあります。1 つは持続的な GPU 余力で、7.6 インチのパネル全面に 120 Hz でピクセル単位のぼかしをかけ続けるのは安くありません。もう 1 つはヒンジそのもので、幾何に追従すると称するトランジションは、報告される角度が滑らかで低遅延でなければ成立しません。粗く量子化されたセンサー値は、どんなシェーダーでも隠せない段付きを生みます。

2. 観察できる振る舞い

  1. 折り目付近に集中する漸進的なぼかし。動作の中間で最大になります。
  2. 同じ帯の暗転。折り目に落ちる影のように見えます。
  3. 部分的な透明化。動いている側が透けて見えるように感じられます。
  4. 遠近の圧縮。パネルが視点から傾くにつれ、内容がヒンジ方向へ狭まります。
  5. 閉じた状態の内容と開いた状態の内容のクロスディゾルブ。

決定的なのは可逆性です。途中で止めれば対応する中間状態で固定され、逆向きに動かせば効果も逆再生されます。時間ベースのアニメーションは、その進行変数が端末の物理的な位置と無関係なので、これができません。

3. 制御信号としてのヒンジ角

Android は API レベル 30 からヒンジ角センサーを公開しています。Sensor.TYPE_HINGE_ANGLE が 2 つの面の角度を度数で返します。レンジや静止値は端末ごとに異なるため、実運用のコードは 0 から 180 と決め打ちせずセンサーオブジェクトから読み取ります。

macOS での対応物は内蔵の開閉角センサーで、ベンダー 0x05AC、プロダクト 0x8104 の IOHIDDevice としてアクセスできます。macTilt はこれを 60 Hz でポーリングし、開始角と終了角を設定可能にしています(たとえば 80 度で開始し 3 度で完了)。Apple はこのデバイスを公開文書化していないため、この方法はリバースエンジニアリングによるものです。

p = clamp( (theta - theta_closed) / (theta_open - theta_closed), 0, 1 )
p_smooth = p_smooth + a * (p_raw - p_smooth),   a はおおよそ 0.25

平滑化を強くしすぎると効果が手の動きから遅れ、ノイズよりも決定的に錯覚を壊します。実機で調整してください。

4. 再構成したシェーダーモデル

以下の式は再構成です。見える効果を再現し、Android 開発者の説明とも整合しますが、公開されていない Apple の実装そのものではありません。

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)

校正の目安として、同じ効果の Three.js 版はソースピクセル基準で最大 72 ピクセルのぼかし半径を使い、暗転はその 2 倍の強度で黒にクランプしています。k = 0.65 なら通常領域は alpha 1.00 付近、折り目の帯は中間で 0.35 程度まで下がります。

外側から順に、通常の面、暗い折り目、細い明線という並びになります。自作の再現版が省きがちな工程で、技術的に正しいぼかしが平板に見える原因はたいていここにあります。

5. 実装

5.1 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 }

    // 180 を決め打ちしない。センサーに聞く。
    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) }
}

SENSOR_DELAY_FASTEST ではなく SENSOR_DELAY_GAME にしているのは、120 Hz では見た目が変わらないのに電力だけ増えるからです。180 のリテラルではなく maximumRange を使うのは、ヒンジの可動域が端末ごとに違うからです。

5.2 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 からの駆動と 2 画面目

val shader = remember { RuntimeShader(FOLD_AGSL) }   // フレームごとに作り直さない

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

両方のサーフェスが同じ StateFlow を読むので、システム内に進行度は 1 つだけ存在します。2 画面を独立に動かすと数フレームでずれ、横から見るとすぐ分かります。RuntimeShader を毎フレーム作り直すのは、この種のコードで最も多いパフォーマンス上の誤りです。

5.4 macOS と Web での対応

構造はそのまま移植できます。macTilt は開閉角センサーを 60 Hz でポーリングし、ScreenCaptureKit でデスクトップを取り込み、Metal Shading Language で折り目を描画します。Web にはヒンジがないため、Three.js の研究版はセンサーをスライダーに置き換え、以降は同一に保っています。プラットフォーム依存なのはパイプラインの最初の段だけです。

6. パフォーマンス

  • 帯の内側だけシェーディングし、マスク外は元のピクセルをそのまま返す。
  • ぼかし前にダウンサンプルする。この半径なら半解像度でも区別できない。
  • p がほとんど変化していないフレームはスキップする。
  • スクリーンショットは動作開始時に 1 回だけ取得し、毎フレーム取り直さない。

7. 再現版の制約

報道の多くは、これを「Apple の機能が Samsung に来た」と表現しました。正確ではありません。2 枚のキャプチャをぼかしでアニメーションさせるアプリであり、その形のままでは SystemUI に組み込めません。システム全体で実現するには、すべてのアプリウィンドウが同じ進行度で同じ合成段に内容を渡す必要があり、それを自分に許可できるのはプラットフォームの持ち主だけです。

8. 視線角度の問題

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

シェーダーはヒンジ角を知っていますが、見る人の目の位置は知りません。正面からなら模擬した遠近と実際の遠近は一致し、横にずれると食い違います。これは実装のバグではなく手法の性質です。埋めるには顔追跡と視点ごとの投影が必要で、壁紙のトランジションには割に合いません。

9. まとめ

Apple の折りたたみアニメーションが優れているのは、ぼかしが難しいからではありません。ソフトウェアの描画が物理的な動きと十分に同期しており、脳が 2 つの画面を 1 つの連続した面として扱ってしまうからです。センサーが時計、シェーダーが描画エンジンであり、錯覚の成否は両者の一致度で決まります。

10. 参考文献

  1. Apple Newsroom「Apple unveils iPhone Duo」2026 年 9 月 9 日。
  2. r/GalaxyFold、iPhone Duo アニメーションの再現、2026 年 9 月 9 日。
  3. lqSky7「iphone-duo-macos-animation (macTilt)」GitHub。
  4. chuspeeism「iphone-duo」GitHub。
  5. 9to5Google、2026 年 9 月 10 日。
  6. Android Developers: Sensor、AGSL、Presentation。