---
title: 材质自动推断与 120Hz 运动降级
description: 指针速度感知、窗口移动与缩放、帧预算探针与省电信号，0ms 降级与 140ms 非对称恢复的抗掉帧架构。
---

# 材质自动推断与 120Hz 运动降级

折射与背景模糊都靠 `backdrop-filter` 采样身后的内容。材质随元素一起移动时，取样会落后于元素的位置，滤镜层拖着一块错位的背景走；在 120Hz（ProMotion）屏幕上拖动窗口时，重滤镜还会挤占本就只有 8.3ms 的帧预算。`TxBaseSurface` 因此在运动期间降级，停下后再恢复。

## 为什么需要运行时自动推断？

客户端的运行环境变化很大：
- 用户可能突然按住标题栏，飞速拖动窗口。
- 笔记本可能在电量低时进入省电模式。
- 老旧核显可能跑不动重度滤镜，连续掉帧。

如果让业务开发者在各个场景里手写 `if (isDragging) { ... }`，代码会迅速腐化，也极易遗漏。TuffEx 把这套判断下沉到 `TxBaseSurface` 的 `autoDetect` 环境感知（默认关闭）。

宿主也可以不开它，自己设置 `moving` 属性。TuffEx 里，`TxBaseAnchor` 的弹出面板在部分打开、关闭动画期间设置它（`surfaceMotionAdaptation` 默认 `auto`），`TxFlipOverlay` 在翻转动画期间设置它，`TxCard` 在惯性跟随（`inertial`）运动期间设置它。

## autoDetect 环境感知信号矩阵

打开 `autoDetect` 后，组件综合以下运行期信号。除第 3 条外都是页面级的：所有打开 `autoDetect` 的材质共用一套监听，监听、电池订阅与帧探针只在至少有一个这样的材质挂载时存在。
1. **指针速度感知**：只看按下状态的指针。相邻两次采样至少间隔 16ms，速度取 $|\vec{v}| = \frac{\|\Delta \vec{s}\|}{\Delta t}$，一次采样超过 $1.2\text{px/ms}$ 即判定为快速移动。没按下的悬停移动不算；按住不动或放慢后，140ms 内没有新的快速采样，信号结束。
2. **窗口移动与缩放**：页面自己的 `resize` 事件自动计入。网页收不到窗口移动的事件，要由宿主转发：在 Electron 里把 `BrowserWindow` 的 `move`（以及 `will-move`）转给渲染进程，调用 `notifySurfaceWindowMotion()`。最后一次报告后 140ms，信号结束。
3. **自身与祖先的 transform**：在材质元素及其所有祖先上监听 `transform` / `translate` 的 `transitionstart`、`transitionend`、`transitioncancel`，只认发生在这些元素自身的过渡，从别处冒泡上来的不算；`MutationObserver` 观察它们的内联 `style`，任一元素带内联 `transform` 即视为运动，全部去掉后视为停止。
4. **帧预算探针**：挂载后趁空闲采样约 30 帧（最多 3 秒），以最快的一成帧间隔估计这块屏幕的帧预算（不超过 16.67ms，120Hz 屏上约 8.3ms）。三分之一以上的帧超过 1.5 个预算记为 `reduced`；中位帧超过 2.5 个预算记为 `minimal`；中位帧超过 250ms 说明页面正被浏览器节流，这次读数作废。降级后依次隔 30、60、120、300 秒重测，恢复后 1.5 秒再确认一次；页面隐藏或失去焦点时不测。
5. **省电信号**：通过 Battery Status API，未充电且电量不高于 20% 视为省电。宿主可以用 `setSurfacePowerSaving(true | false)` 覆盖（例如接 Electron 的 `powerMonitor`），传 `null` 交还给电池接口。

前三条是运动信号，决定**何时**降级；后两条决定**渲染哪种材质**：省电或 `reduced` 时，`refraction`、`glass`、`acrylic` 换成 `blur`；`minimal` 时，所有采样背景的材质（含 `blur`）换成纯色 `flat`。`mica`、`plain`、`pure`、`mask` 不受影响。

`getSurfaceEnvironment()` 以只读 ref 给出页面当前的 `pointerMoving`、`windowMoving`、`powerSaving` 与 `frameTier`，阈值以 `SURFACE_POINTER_SPEED_THRESHOLD`、`SURFACE_MOTION_SETTLE_MS`、`SURFACE_LOW_BATTERY_LEVEL` 导出。

## 0ms 降级与 140ms 非对称恢复

为了消除运动中的卡顿，TuffEx 采用**非对称时钟**：
- **降级（0ms）**：任一运动信号出现，当帧降级，不做防抖。重滤镜立刻让出 GPU，拖动跟手。
- **恢复（140ms 迟滞）**：信号消失后不立即切回，防止手势在阈值附近轻微颤抖时反复切换；等 140ms 内没有新信号，才开始恢复。

这个时钟以 `useSurfaceMotionGuard(emitFallback, { settleMs })` 导出：`onMotionStart()` 立即回调 `true`，`onMotionEnd()` 在安静满 `settleMs`（默认 140ms）后回调 `false`。知道自己何时运动的宿主，可以用它驱动 `moving`。

## 降级快，恢复分层

下文的“运动结束”，对 `autoDetect` 而言是 140ms 迟滞之后；对 `moving` 而言是宿主把它置回 `false` 的时刻。

`blur`、`glass` 与 `acrylic` 整体换材质：
- **降级**：运动一开始，渲染模式立即换成 `fallbackMode`。默认是 `mask`，色膜不透明度取 `fallbackMaskOpacity`（`TxBaseSurface` 默认 0.95，`TxCard` 传 0.26）；也可以设为 `pure`。
- **恢复**：运动结束后先保持降级 `max(transitionDuration, settleDelay)`，默认 `max(299, 150)` = 299ms，再换回原材质。

`refraction` 不换材质，而是分层降级：
- **降级**：玻璃层和滤镜层在 140ms 内淡出，一层半透明遮盖在 180ms 内升到 0.55，折射参数的混合进度降到 0.28，折射描边立即隐藏。
- **恢复**：运动一结束就开始，没有等待期。参数在 182ms 内回满；遮盖先停 60ms，再用 227ms 撤完，到 287ms 结束；玻璃层和滤镜层延迟 80ms 后用 220ms 淡回；恢复结束后再过 60ms，描边用 220ms 淡回。恢复途中再次运动，会直接回到降级状态。

`pure`、`mask`、`flat`、`mica`、`plain` 没有采样背景的滤镜层，运动时不做任何处理。

:::TuffDemoWrapper{demo="ConceptSurfaceDeductionDemo"}
:::

## 状态机转换图

```mermaid
stateDiagram-v2
    [*] --> Refraction_Active: mode = refraction，环境正常
    Refraction_Active --> Layered_Fallback: 运动信号出现（0ms）
    Layered_Fallback --> Refraction_Recovering: 信号安静满 140ms
    Refraction_Recovering --> Layered_Fallback: 恢复途中再次运动
    Refraction_Recovering --> Refraction_Active: 287ms 后（描边再过 60 + 220ms 淡回）
    Refraction_Active --> Blur_Degraded: 省电，或帧预算 reduced
    Blur_Degraded --> Refraction_Active: 省电解除且帧预算回到 ok
    Blur_Degraded --> Fallback_Mode: 运动信号出现（0ms，换成 fallbackMode）
    Fallback_Mode --> Settling: 信号安静满 140ms
    Settling --> Fallback_Mode: 再次运动
    Settling --> Blur_Degraded: 299ms 后
    Refraction_Active --> Flat: 帧预算 minimal
    Blur_Degraded --> Flat: 帧预算 minimal
    Flat --> Refraction_Active: 重测回到 ok
```
`mode` 本身就是 `blur`、`glass` 或 `acrylic` 的材质，静止时保持原材质，运动时同样走 `Fallback_Mode` 与 `Settling` 这一圈。运动中的材质因此不会拖着错位的背景走，停下后又回到完整的折射。
