mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-10 10:16:44 +08:00
perf(glass): skip hidden scroll surface output during native scrolling
This commit is contained in:
@@ -3037,6 +3037,14 @@ describe('glass optical surface discovery', () => {
|
||||
callbacks.clear()
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now()))
|
||||
}
|
||||
const mainScene = render.mock.calls
|
||||
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||
.find(scene => scene.children[0]?.material?.uniforms.uPointer)
|
||||
if (!mainScene) throw new Error('main optical scene was not rendered')
|
||||
const uniforms = mainScene.children[0].material!.uniforms as unknown as {
|
||||
uPointer: { value: { x: number; y: number } }
|
||||
uTrail: { value: Array<{ x: number; y: number; z: number }> }
|
||||
}
|
||||
render.mockClear()
|
||||
|
||||
dispatchTouchEvent('touchstart', [{ clientX: 80, clientY: 180, identifier: 7 }])
|
||||
@@ -3050,18 +3058,9 @@ describe('glass optical surface discovery', () => {
|
||||
callbacks.clear()
|
||||
interactionFrames.forEach(callback => callback(moveEvent.timeStamp + 16))
|
||||
|
||||
const scene = render.mock.calls.at(-1)?.[0] as unknown as {
|
||||
children: Array<{
|
||||
material: {
|
||||
uniforms: {
|
||||
uPointer: { value: { x: number; y: number } }
|
||||
uTrail: { value: Array<{ x: number; y: number; z: number }> }
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
const pointer = scene.children[0].material.uniforms.uPointer.value
|
||||
const trail = scene.children[0].material.uniforms.uTrail.value
|
||||
expect(render).not.toHaveBeenCalled()
|
||||
const pointer = uniforms.uPointer.value
|
||||
const trail = uniforms.uTrail.value
|
||||
expect(pointer.x).toBeCloseTo(180 / 390)
|
||||
expect(pointer.y).toBeCloseTo(1 - 320 / 844)
|
||||
expect(trail[0]).toMatchObject({ z: 1 })
|
||||
@@ -3633,6 +3632,16 @@ describe('glass optical surface discovery', () => {
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now()))
|
||||
}
|
||||
expect(callbacks.size).toBe(0)
|
||||
const mainScene = render.mock.calls
|
||||
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||
.find(scene => scene.children[0]?.material?.uniforms.uScrollOffset)
|
||||
if (!mainScene) throw new Error('main optical scene was not rendered')
|
||||
const settledUniforms = mainScene.children[0].material!.uniforms as unknown as {
|
||||
uMotion: { value: number }
|
||||
uRects: { value: Array<{ y: number }> }
|
||||
uScrollOffset: { value: { y: number } }
|
||||
uTrail: { value: Array<{ z: number }> }
|
||||
}
|
||||
|
||||
vi.spyOn(window, 'scrollY', 'get').mockReturnValue(120)
|
||||
setOpticalSurfaceBounds(surface, { height: 500, width: 350, x: 20, y: -20 })
|
||||
@@ -3643,19 +3652,6 @@ describe('glass optical surface discovery', () => {
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + 160 + pass * 16))
|
||||
}
|
||||
|
||||
const settledScene = render.mock.calls.at(-1)?.[0] as unknown as {
|
||||
children: Array<{
|
||||
material: {
|
||||
uniforms: {
|
||||
uMotion: { value: number }
|
||||
uRects: { value: Array<{ y: number }> }
|
||||
uScrollOffset: { value: { y: number } }
|
||||
uTrail: { value: Array<{ z: number }> }
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
const settledUniforms = settledScene.children[0].material.uniforms
|
||||
expect(settledUniforms.uMotion.value).toBe(0)
|
||||
expect(settledUniforms.uTrail.value.every(trail => trail.z === 0)).toBe(true)
|
||||
expect(settledUniforms.uRects.value[0].y).toBeCloseTo(1 - (100 + 500) / 844)
|
||||
@@ -3705,6 +3701,15 @@ describe('glass optical surface discovery', () => {
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||
}
|
||||
expect(callbacks.size).toBe(0)
|
||||
const mainScene = render.mock.calls
|
||||
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||
.find(scene => scene.children[0]?.material?.uniforms.uScrollOffset)
|
||||
if (!mainScene) throw new Error('main optical scene was not rendered')
|
||||
const uniforms = mainScene.children[0].material!.uniforms as unknown as {
|
||||
uMotion: { value: number }
|
||||
uScrollOffset: { value: { y: number } }
|
||||
uTrail: { value: Array<{ z: number }> }
|
||||
}
|
||||
render.mockClear()
|
||||
|
||||
scrollY = 80
|
||||
@@ -3719,18 +3724,6 @@ describe('glass optical surface discovery', () => {
|
||||
callbacks.clear()
|
||||
scheduledCallbacks.forEach(callback => {
|
||||
callback(performance.now() + 100 + pass * 16)
|
||||
const scene = render.mock.calls.at(-1)?.[0] as unknown as {
|
||||
children: Array<{
|
||||
material: {
|
||||
uniforms: {
|
||||
uMotion: { value: number }
|
||||
uScrollOffset: { value: { y: number } }
|
||||
uTrail: { value: Array<{ z: number }> }
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
const uniforms = scene.children[0].material.uniforms
|
||||
renderedScrollOffsets.push(uniforms.uScrollOffset.value.y)
|
||||
expect(uniforms.uMotion.value).toBe(0)
|
||||
expect(uniforms.uTrail.value.every(trail => trail.z === 0)).toBe(true)
|
||||
@@ -3763,21 +3756,6 @@ describe('glass optical surface discovery', () => {
|
||||
vi.spyOn(window, 'scrollY', 'get').mockImplementation(() => scrollY)
|
||||
const three = await import('three')
|
||||
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||
const wallpaperSampling: number[] = []
|
||||
render.mockImplementation(scene => {
|
||||
const uniforms = (
|
||||
scene as unknown as {
|
||||
children: Array<{
|
||||
material?: {
|
||||
uniforms?: {
|
||||
uHasWallpaperTexture?: { value: number }
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
).children[0]?.material?.uniforms
|
||||
if (uniforms?.uHasWallpaperTexture) wallpaperSampling.push(uniforms.uHasWallpaperTexture.value)
|
||||
})
|
||||
const callbacks = new Map<number, FrameRequestCallback>()
|
||||
let frameId = 0
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
|
||||
@@ -3808,11 +3786,16 @@ describe('glass optical surface discovery', () => {
|
||||
callbacks.clear()
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||
}
|
||||
expect(wallpaperSampling.at(-1)).toBe(1)
|
||||
const mainScene = render.mock.calls
|
||||
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||
.find(scene => scene.children[0]?.material?.uniforms.uHasWallpaperTexture)
|
||||
if (!mainScene) throw new Error('main optical scene was not rendered')
|
||||
const wallpaperSampling = mainScene.children[0].material!.uniforms.uHasWallpaperTexture
|
||||
expect(wallpaperSampling.value).toBe(1)
|
||||
|
||||
window.dispatchEvent(new WheelEvent('wheel', { deltaY: 80 }))
|
||||
|
||||
expect(wallpaperSampling.at(-1)).toBe(0)
|
||||
expect(wallpaperSampling.value).toBe(0)
|
||||
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
|
||||
|
||||
scrollY = 240
|
||||
@@ -3823,13 +3806,152 @@ describe('glass optical surface discovery', () => {
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + 100 + pass * 16))
|
||||
}
|
||||
|
||||
expect(wallpaperSampling.at(-1)).toBe(1)
|
||||
expect(wallpaperSampling.value).toBe(1)
|
||||
expect(document.documentElement).not.toHaveAttribute('data-glass-scroll-presentation')
|
||||
expect(callbacks.size).toBe(0)
|
||||
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it('skips hidden native scroll draws and commits one frame before revealing the canvas', async () => {
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
||||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
||||
let scrollY = 0
|
||||
vi.spyOn(window, 'scrollY', 'get').mockImplementation(() => scrollY)
|
||||
const three = await import('three')
|
||||
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||
const callbacks = new Map<number, FrameRequestCallback>()
|
||||
let frameId = 0
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
|
||||
frameId += 1
|
||||
callbacks.set(frameId, callback)
|
||||
|
||||
return frameId
|
||||
})
|
||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(id => callbacks.delete(id))
|
||||
appendOpticalSurface('app-hover-lift-card', { height: 300, width: 400, x: 40, y: 120 })
|
||||
const scope = effectScope()
|
||||
const renderer = scope.run(() =>
|
||||
useGlassOpticalRenderer({
|
||||
active: ref(true),
|
||||
appearance: ref('clear'),
|
||||
canvas: ref(document.createElement('canvas')),
|
||||
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('off'),
|
||||
quality: ref('high'),
|
||||
routeKey: ref('/dashboard'),
|
||||
surfaceSpace: 'scroll',
|
||||
tintColor: ref('#8D51F9'),
|
||||
wallpaperUrl: ref('/api/v1/login/wallpapers/opaque-id'),
|
||||
}),
|
||||
)
|
||||
|
||||
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||
expect(document.documentElement.dataset.glassRendererState).toBe('ready')
|
||||
const mainScene = render.mock.calls
|
||||
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||
.find(scene => scene.children[0]?.material?.uniforms.uScrollOffset)
|
||||
if (!mainScene) throw new Error('main optical scene was not rendered')
|
||||
const uniforms = mainScene.children[0].material!.uniforms
|
||||
const framesBeforeNativeScroll = renderer?.renderedFrames.value
|
||||
let presentationAtRestoreRender: string | undefined
|
||||
render.mockImplementation(() => {
|
||||
presentationAtRestoreRender = document.documentElement.dataset.glassScrollPresentation
|
||||
})
|
||||
render.mockClear()
|
||||
|
||||
window.dispatchEvent(new WheelEvent('wheel', { deltaY: 80 }))
|
||||
|
||||
expect(render).not.toHaveBeenCalled()
|
||||
expect(renderer?.renderedFrames.value).toBe(framesBeforeNativeScroll)
|
||||
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
|
||||
|
||||
scrollY = 240
|
||||
window.dispatchEvent(new Event('scroll'))
|
||||
const firstScrollFrame = [...callbacks.values()][0]
|
||||
callbacks.clear()
|
||||
firstScrollFrame?.(performance.now() + 100)
|
||||
|
||||
expect(render).not.toHaveBeenCalled()
|
||||
expect(uniforms.uScrollOffset.value.y).toBe(240)
|
||||
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
|
||||
|
||||
const secondScrollFrame = [...callbacks.values()][0]
|
||||
callbacks.clear()
|
||||
secondScrollFrame?.(performance.now() + 116)
|
||||
|
||||
expect(render).not.toHaveBeenCalled()
|
||||
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
|
||||
|
||||
const restoreFrame = [...callbacks.values()][0]
|
||||
callbacks.clear()
|
||||
restoreFrame?.(performance.now() + 132)
|
||||
|
||||
expect(render).toHaveBeenCalledTimes(1)
|
||||
expect(presentationAtRestoreRender).toBe('native')
|
||||
expect(renderer?.renderedFrames.value).toBe((framesBeforeNativeScroll ?? 0) + 1)
|
||||
expect(document.documentElement).not.toHaveAttribute('data-glass-scroll-presentation')
|
||||
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it.each(['loading', 'fallback'] as const)(
|
||||
'keeps drawing while the scroll context is ready but the composite renderer state is %s',
|
||||
async rootState => {
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
||||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
||||
const three = await import('three')
|
||||
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||
const callbacks = new Map<number, FrameRequestCallback>()
|
||||
let frameId = 0
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
|
||||
frameId += 1
|
||||
callbacks.set(frameId, callback)
|
||||
|
||||
return frameId
|
||||
})
|
||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(id => callbacks.delete(id))
|
||||
appendOpticalSurface('app-hover-lift-card', { height: 300, width: 400, x: 40, y: 120 })
|
||||
const scope = effectScope()
|
||||
const renderer = scope.run(() =>
|
||||
useGlassOpticalRenderer({
|
||||
active: ref(true),
|
||||
appearance: ref('clear'),
|
||||
canvas: ref(document.createElement('canvas')),
|
||||
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('off'),
|
||||
quality: ref('high'),
|
||||
routeKey: ref('/dashboard'),
|
||||
surfaceSpace: 'scroll',
|
||||
tintColor: ref('#8D51F9'),
|
||||
wallpaperUrl: ref('/api/v1/login/wallpapers/opaque-id'),
|
||||
}),
|
||||
)
|
||||
|
||||
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||
for (let pass = 0; pass < 4 && callbacks.size > 0; pass += 1) {
|
||||
const scheduledCallbacks = [...callbacks.values()]
|
||||
callbacks.clear()
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||
}
|
||||
render.mockClear()
|
||||
|
||||
window.dispatchEvent(new WheelEvent('wheel', { deltaY: 80 }))
|
||||
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
|
||||
expect(render).not.toHaveBeenCalled()
|
||||
|
||||
document.documentElement.dataset.glassRendererState = rootState
|
||||
window.dispatchEvent(new Event('scroll'))
|
||||
const firstScrollFrame = [...callbacks.values()][0]
|
||||
callbacks.clear()
|
||||
firstScrollFrame?.(performance.now() + 100)
|
||||
|
||||
expect(renderer?.state.value).toBe('ready')
|
||||
expect(document.documentElement.dataset.glassRendererState).toBe(rootState)
|
||||
expect(render).toHaveBeenCalledTimes(1)
|
||||
|
||||
scope.stop()
|
||||
},
|
||||
)
|
||||
|
||||
it('clears ripple state before native scroll presentation takes ownership', async () => {
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
||||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
||||
@@ -4120,6 +4242,14 @@ describe('glass optical surface discovery', () => {
|
||||
callbacks.clear()
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||
}
|
||||
const mainScene = render.mock.calls
|
||||
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||
.find(scene => scene.children[0]?.material?.uniforms.uRects)
|
||||
if (!mainScene) throw new Error('main optical scene was not rendered')
|
||||
const uniforms = mainScene.children[0].material!.uniforms as unknown as {
|
||||
uRects: { value: Array<{ y: number }> }
|
||||
}
|
||||
const initialRectY = uniforms.uRects.value[0]?.y
|
||||
render.mockClear()
|
||||
setRenderTarget.mockClear()
|
||||
|
||||
@@ -4135,7 +4265,8 @@ describe('glass optical surface discovery', () => {
|
||||
callbacks.clear()
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + 100))
|
||||
|
||||
expect(render).toHaveBeenCalled()
|
||||
expect(render).not.toHaveBeenCalled()
|
||||
expect(uniforms.uRects.value[0]?.y).not.toBe(initialRectY)
|
||||
expect(setRenderTarget).not.toHaveBeenCalled()
|
||||
|
||||
const transitionEnd = new Event('transitionend', { bubbles: true }) as TransitionEvent
|
||||
@@ -4199,6 +4330,13 @@ describe('glass optical surface discovery', () => {
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||
}
|
||||
expect(callbacks.size).toBe(0)
|
||||
const mainScene = render.mock.calls
|
||||
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||
.find(scene => scene.children[0]?.material?.uniforms.uRects)
|
||||
if (!mainScene) throw new Error('main optical scene was not rendered')
|
||||
const scrollUniforms = mainScene.children[0].material!.uniforms as unknown as {
|
||||
uRects: { value: Array<{ y: number }> }
|
||||
}
|
||||
querySelectorAll.mockClear()
|
||||
|
||||
scrollY = 1000
|
||||
@@ -4209,16 +4347,7 @@ describe('glass optical surface discovery', () => {
|
||||
const firstScrollFrame = [...callbacks.values()]
|
||||
callbacks.clear()
|
||||
firstScrollFrame.forEach(callback => callback(performance.now() + 100))
|
||||
const firstScrollScene = render.mock.calls.at(-1)?.[0] as unknown as {
|
||||
children: Array<{
|
||||
material: {
|
||||
uniforms: {
|
||||
uRects: { value: Array<{ y: number }> }
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
expect(firstScrollScene.children[0].material.uniforms.uRects.value[0].y).toBeCloseTo(1 - (1100 + 240) / 2000)
|
||||
expect(scrollUniforms.uRects.value[0].y).toBeCloseTo(1 - (1100 + 240) / 2000)
|
||||
expect(querySelectorAll).not.toHaveBeenCalled()
|
||||
|
||||
for (let pass = 0; pass < 3 && callbacks.size > 0; pass += 1) {
|
||||
@@ -4303,6 +4432,13 @@ describe('glass optical surface discovery', () => {
|
||||
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||
}
|
||||
expect(callbacks.size).toBe(0)
|
||||
const mainScene = render.mock.calls
|
||||
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||
.find(scene => scene.children[0]?.material?.uniforms.uRects)
|
||||
if (!mainScene) throw new Error('main optical scene was not rendered')
|
||||
const uniforms = mainScene.children[0].material!.uniforms as unknown as {
|
||||
uRects: { value: Array<{ y: number }> }
|
||||
}
|
||||
querySelectorAll.mockClear()
|
||||
|
||||
setOpticalSurfaceBounds(surface, { height: 300, width: 400, x: 40, y: 180 })
|
||||
@@ -4311,17 +4447,7 @@ describe('glass optical surface discovery', () => {
|
||||
const firstScrollFrame = [...callbacks.values()]
|
||||
callbacks.clear()
|
||||
firstScrollFrame.forEach(callback => callback(performance.now() + 100))
|
||||
|
||||
const firstScrollScene = render.mock.calls.at(-1)?.[0] as unknown as {
|
||||
children: Array<{
|
||||
material: {
|
||||
uniforms: {
|
||||
uRects: { value: Array<{ y: number }> }
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
expect(firstScrollScene.children[0].material.uniforms.uRects.value[0].y).toBeCloseTo(1 - (180 + 300) / 1200)
|
||||
expect(uniforms.uRects.value[0].y).toBeCloseTo(1 - (180 + 300) / 1200)
|
||||
expect(querySelectorAll).toHaveBeenCalled()
|
||||
|
||||
scope.stop()
|
||||
|
||||
@@ -1827,6 +1827,17 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
if (fluidDynamics && advanceFlow) {
|
||||
resources.uniforms.uFlowTexture.value = fluidDynamics.step()
|
||||
}
|
||||
// 原生 backdrop 接管期间 scroll canvas 已由 CSS 隐藏;保留事务、流场和 uniform 同步,跳过主材质输出。
|
||||
if (
|
||||
presentationSpace === 'scroll' &&
|
||||
scrollWallpaperSamplingSuppressed &&
|
||||
state.value === 'ready' &&
|
||||
document.documentElement.dataset.glassRendererState === 'ready' &&
|
||||
document.documentElement.dataset.glassScrollPresentation === 'native'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (presentationSpace === 'scroll') {
|
||||
const { height: presentationHeight } = getCommittedPresentationSize()
|
||||
const scaleY = presentationBufferHeight / Math.max(presentationHeight, 1)
|
||||
|
||||
Reference in New Issue
Block a user