组件/设计工程概念手册/材质自动推断与 120Hz 运动降级

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

指针速度感知、窗口移动与缩放、帧预算探针与省电信号,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,速度取 ∣v⃗∣=∥Δs⃗∥Δt|\vec{v}| = \frac{\|\Delta \vec{s}\|}{\Delta t},一次采样超过 1.2px/ms1.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 没有采样背景的滤镜层,运动时不做任何处理。

示例加载中...

状态机转换图

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 这一圈。运动中的材质因此不会拖着错位的背景走,停下后又回到完整的折射。