mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-10 10:16:44 +08:00
fix(glass): restore refraction when navbar motion settles
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user