fix(glass): restore refraction when navbar motion settles

This commit is contained in:
InfinityPacer
2026-09-06 03:13:34 +08:00
parent a9c804e724
commit 791b20a303
2 changed files with 88 additions and 12 deletions
@@ -48,6 +48,7 @@ let transparencyQuery: MediaQueryList | null = null
let mapRevision = 0 let mapRevision = 0
let cachedGeometry = '' let cachedGeometry = ''
let cachedMap = NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP let cachedMap = NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP
let pendingGeometry = ''
let failedGeometry = '' let failedGeometry = ''
let lastObservedGeometry = '' let lastObservedGeometry = ''
const geometryTransitions = new Set<string>() const geometryTransitions = new Set<string>()
@@ -69,6 +70,7 @@ function isRefractionActive() {
/** 几何或状态变化后立即撤销旧 map,避免在另一个尺寸中采样。 */ /** 几何或状态变化后立即撤销旧 map,避免在另一个尺寸中采样。 */
function invalidateDisplacementMap() { function invalidateDisplacementMap() {
mapRevision += 1 mapRevision += 1
pendingGeometry = ''
observedShell?.setAttribute('data-glass-navbar-refraction-ready', 'false') observedShell?.setAttribute('data-glass-navbar-refraction-ready', 'false')
} }
@@ -94,11 +96,9 @@ function handleStateMutations(records: MutationRecord[]) {
if (records.some(hasObservedSizeStyleChange)) scheduleDisplacementMapSync() if (records.some(hasObservedSizeStyleChange)) scheduleDisplacementMapSync()
} }
/** map 与 feImage 尺寸同批更新;解码失败或过期结果继续使用 CSS 材质。 */ /** 以真实边界和计算后的圆角统一比较已解码、待解码与当前采样几何。 */
async function syncDisplacementMap() { function readDisplacementGeometry() {
if (!observedNavbar || !isRefractionActive() || geometryTransitions.size > 0) return if (!observedNavbar) return null
const revision = ++mapRevision
const bounds = observedNavbar.getBoundingClientRect() const bounds = observedNavbar.getBoundingClientRect()
const styles = getComputedStyle(observedNavbar) const styles = getComputedStyle(observedNavbar)
// 自定义属性可能保留 rem;只有计算后的圆角与位移图使用同一 CSS 像素坐标。 // 自定义属性可能保留 rem;只有计算后的圆角与位移图使用同一 CSS 像素坐标。
@@ -108,7 +108,23 @@ async function syncDisplacementMap() {
const radius = Number.isFinite(borderRadius) ? borderRadius : DEFAULT_NAVBAR_GEOMETRY.radius const radius = Number.isFinite(borderRadius) ? borderRadius : DEFAULT_NAVBAR_GEOMETRY.radius
const optics = opticalResponse.value const optics = opticalResponse.value
const geometryKey = `${width}:${height}:${radius}:${optics.horizontalRatio}:${optics.verticalRatio}:${optics.translationPx}` return {
height,
radius,
width,
optics,
key: `${width}:${height}:${radius}:${optics.horizontalRatio}:${optics.verticalRatio}:${optics.translationPx}`,
}
}
/** map 与 feImage 尺寸同批更新;解码失败或过期结果继续使用 CSS 材质。 */
async function syncDisplacementMap() {
if (!isRefractionActive() || geometryTransitions.size > 0) return
const geometry = readDisplacementGeometry()
if (!geometry || pendingGeometry === geometry.key) return
const { height, radius, width, optics, key: geometryKey } = geometry
const revision = ++mapRevision
pendingGeometry = geometryKey
if (lastObservedGeometry !== geometryKey) { if (lastObservedGeometry !== geometryKey) {
lastObservedGeometry = geometryKey lastObservedGeometry = geometryKey
failedGeometry = '' failedGeometry = ''
@@ -144,14 +160,28 @@ async function syncDisplacementMap() {
failedGeometry = geometryKey failedGeometry = geometryKey
invalidateDisplacementMap() invalidateDisplacementMap()
} }
} finally {
if (revision === mapRevision) pendingGeometry = ''
} }
} }
// 动画期间使用同族 CSS 材质;尺寸稳定后只重建一次,不逐帧生成或拉伸旧图。 // 连续 resize 需要合并;相同几何的通知不撤销已就绪或正在解码的位移图。
function scheduleDisplacementMapSync() { function scheduleDisplacementMapSync() {
invalidateDisplacementMap()
if (resizeTimer !== null) clearTimeout(resizeTimer) if (resizeTimer !== null) clearTimeout(resizeTimer)
if (!isRefractionActive()) return resizeTimer = null
if (!isRefractionActive() || geometryTransitions.size > 0) {
invalidateDisplacementMap()
return
}
const geometry = readDisplacementGeometry()
if (geometry?.key === pendingGeometry) return
if (geometry?.key === cachedGeometry && displacementMapUrl.value === cachedMap) {
// 取消草稿可能命中旧缓存,同时还有另一参数的解码;先使该异步结果失效。
if (pendingGeometry) invalidateDisplacementMap()
observedShell?.setAttribute('data-glass-navbar-refraction-ready', 'true')
return
}
invalidateDisplacementMap()
resizeTimer = setTimeout(() => { resizeTimer = setTimeout(() => {
resizeTimer = null resizeTimer = null
void syncDisplacementMap() void syncDisplacementMap()
@@ -169,7 +199,12 @@ function handleGeometryTransition(event: TransitionEvent) {
invalidateDisplacementMap() invalidateDisplacementMap()
} else { } else {
geometryTransitions.delete(event.propertyName) geometryTransitions.delete(event.propertyName)
if (geometryTransitions.size === 0) scheduleDisplacementMapSync() // transitionend/cancel 已给出稳定尺寸,无需再附加 resize 防抖等待。
if (geometryTransitions.size === 0) {
if (resizeTimer !== null) clearTimeout(resizeTimer)
resizeTimer = null
void syncDisplacementMap()
}
} }
} }
@@ -132,15 +132,16 @@ describe('GlassNavbarRefractionDefs', () => {
expectReadyForWidth(1423) expectReadyForWidth(1423)
}) })
it('disables old sampling during resize and caches unchanged geometry', async () => { it('keeps unchanged geometry ready but disables old sampling when the size changes', async () => {
wrapper = mount(GlassNavbarRefractionDefs) wrapper = mount(GlassNavbarRefractionDefs)
await settle() await settle()
resize?.([], {} as ResizeObserver) resize?.([], {} as ResizeObserver)
expect(shell.dataset.glassNavbarRefractionReady).toBe('false') expect(shell.dataset.glassNavbarRefractionReady).toBe('true')
await settle() await settle()
expect(createGlassNavbarDisplacementMap).toHaveBeenCalledTimes(1) expect(createGlassNavbarDisplacementMap).toHaveBeenCalledTimes(1)
width = 1200 width = 1200
resize?.([], {} as ResizeObserver) resize?.([], {} as ResizeObserver)
expect(shell.dataset.glassNavbarRefractionReady).toBe('false')
await settle() await settle()
expect(createGlassNavbarDisplacementMap).toHaveBeenLastCalledWith( expect(createGlassNavbarDisplacementMap).toHaveBeenLastCalledWith(
expect.objectContaining({ width: 1200, height: 64, radius: 16 }), expect.objectContaining({ width: 1200, height: 64, radius: 16 }),
@@ -148,6 +149,46 @@ describe('GlassNavbarRefractionDefs', () => {
expectReadyForWidth(1200) expectReadyForWidth(1200)
}) })
it('restores a cached map immediately when the final geometry transition ends', async () => {
wrapper = mount(GlassNavbarRefractionDefs)
await settle()
dispatchTransition('transitionrun', 'inset-inline-start')
dispatchTransition('transitionrun', 'border-radius')
dispatchTransition('transitionend', 'inset-inline-start')
await flushPromises()
expect(shell.dataset.glassNavbarRefractionReady).toBe('false')
dispatchTransition('transitionend', 'border-radius')
await flushPromises()
expectReadyForWidth(1423)
expect(createGlassNavbarDisplacementMap).toHaveBeenCalledTimes(1)
})
it('does not cancel a pending decode on a duplicate resize notification', async () => {
wrapper = mount(GlassNavbarRefractionDefs)
await vi.advanceTimersByTimeAsync(65)
resize?.([], {} as ResizeObserver)
completePendingDecode()
await flushPromises()
expectReadyForWidth(1423)
expect(createGlassNavbarDisplacementMap).toHaveBeenCalledTimes(1)
})
it('rejects a late draft decode when cancellation already restored the cached map', async () => {
vi.mocked(createGlassNavbarDisplacementMap)
.mockReturnValueOnce('data:image/png;base64,saved')
.mockReturnValueOnce('data:image/png;base64,draft')
wrapper = mount(GlassNavbarRefractionDefs)
await settle()
effectiveSettings.value = { glassDeformationStrength: 99, glassTranslationStrength: 99 }
await vi.advanceTimersByTimeAsync(65)
expect(createGlassNavbarDisplacementMap).toHaveBeenCalledTimes(2)
effectiveSettings.value = { glassDeformationStrength: 48, glassTranslationStrength: 48 }
expectReadyForWidth(1423)
completePendingDecode()
await flushPromises()
expect(wrapper.get('feImage').attributes('href')).toBe('data:image/png;base64,saved')
})
it('does not generate a map in CSS quality', async () => { it('does not generate a map in CSS quality', async () => {
document.documentElement.dataset.glassQuality = 'css' document.documentElement.dataset.glassQuality = 'css'
wrapper = mount(GlassNavbarRefractionDefs) wrapper = mount(GlassNavbarRefractionDefs)