mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-10 10:16:44 +08:00
perf(glass): skip unused shader samples and gate paused rendering
This commit is contained in:
@@ -92,6 +92,27 @@ class ResizeObserverMock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提供可显式触发 DOM 变更回调的 MutationObserver 实现。 */
|
||||||
|
class MutationObserverTriggerMock implements MutationObserver {
|
||||||
|
static instances: MutationObserverTriggerMock[] = []
|
||||||
|
|
||||||
|
constructor(private readonly callback: MutationCallback) {
|
||||||
|
MutationObserverTriggerMock.instances.push(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {}
|
||||||
|
|
||||||
|
observe() {}
|
||||||
|
|
||||||
|
takeRecords() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger(records: MutationRecord[]) {
|
||||||
|
this.callback(records, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 创建带稳定视口边界的光学表面元素。 */
|
/** 创建带稳定视口边界的光学表面元素。 */
|
||||||
function appendOpticalSurface(className: string, bounds: Pick<DOMRect, 'height' | 'width' | 'x' | 'y'>) {
|
function appendOpticalSurface(className: string, bounds: Pick<DOMRect, 'height' | 'width' | 'x' | 'y'>) {
|
||||||
const surface = document.createElement('section')
|
const surface = document.createElement('section')
|
||||||
@@ -170,6 +191,30 @@ function createRemovalRecord(target: Element, removedNodes: Element[]): Mutation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 构造会改变光学表面资格的属性变更记录。 */
|
||||||
|
function createAttributeRecord(target: Element): MutationRecord {
|
||||||
|
return {
|
||||||
|
addedNodes: Object.assign([], { item: () => null }) as unknown as NodeList,
|
||||||
|
attributeName: 'data-glass-optical-mode',
|
||||||
|
attributeNamespace: null,
|
||||||
|
nextSibling: null,
|
||||||
|
oldValue: null,
|
||||||
|
previousSibling: null,
|
||||||
|
removedNodes: Object.assign([], { item: () => null }) as unknown as NodeList,
|
||||||
|
target,
|
||||||
|
type: 'attributes',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 执行有限数量的排队帧,避免交互衰减帧让测试进入无界循环。 */
|
||||||
|
function flushQueuedAnimationFrames(callbacks: Map<number, FrameRequestCallback>, passes = 8) {
|
||||||
|
for (let pass = 0; pass < passes && callbacks.size > 0; pass += 1) {
|
||||||
|
const queued = [...callbacks.values()]
|
||||||
|
callbacks.clear()
|
||||||
|
queued.forEach(callback => callback(performance.now() + pass * 16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function dispatchTouchEvent(
|
function dispatchTouchEvent(
|
||||||
type: 'touchcancel' | 'touchend' | 'touchmove' | 'touchstart',
|
type: 'touchcancel' | 'touchend' | 'touchmove' | 'touchstart',
|
||||||
touches: Array<{ clientX: number; clientY: number; identifier: number }>,
|
touches: Array<{ clientX: number; clientY: number; identifier: number }>,
|
||||||
@@ -185,8 +230,12 @@ function dispatchTouchEvent(
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let documentHasFocus = true
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ResizeObserverMock.instances = []
|
ResizeObserverMock.instances = []
|
||||||
|
MutationObserverTriggerMock.instances = []
|
||||||
|
documentHasFocus = true
|
||||||
wallpaperToneMocks.load.mockReset()
|
wallpaperToneMocks.load.mockReset()
|
||||||
wallpaperToneMocks.load.mockResolvedValue({
|
wallpaperToneMocks.load.mockResolvedValue({
|
||||||
corsReady: false,
|
corsReady: false,
|
||||||
@@ -200,6 +249,7 @@ beforeEach(() => {
|
|||||||
wallpaperToneMocks.takeDecodedSource.mockReturnValue(undefined)
|
wallpaperToneMocks.takeDecodedSource.mockReturnValue(undefined)
|
||||||
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
|
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
|
||||||
vi.stubGlobal('WebGLRenderingContext', class {})
|
vi.stubGlobal('WebGLRenderingContext', class {})
|
||||||
|
vi.spyOn(document, 'hasFocus').mockImplementation(() => documentHasFocus)
|
||||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null)
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1344,6 +1394,48 @@ describe('glass optical surface discovery', () => {
|
|||||||
scope.stop()
|
scope.stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps stable texture sampling lazy and limits diffuse taps to non-prefiltered frosted output', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(document.createElement('canvas')),
|
||||||
|
quality: ref('high'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
const scene = render.mock.calls
|
||||||
|
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||||
|
.find(candidate => candidate.children[0]?.material?.uniforms.uTextureMix)
|
||||||
|
if (!scene) throw new Error('main optical scene was not rendered')
|
||||||
|
const material = scene.children[0].material!
|
||||||
|
const shader = material.fragmentShader
|
||||||
|
const previousToneIndex = shader.indexOf('toneMapWallpaper(previous, viewportUv, uPreviousWallpaperExposure)')
|
||||||
|
const currentToneIndex = shader.indexOf('toneMapWallpaper(current, viewportUv, uWallpaperExposure)')
|
||||||
|
const mixIndex = shader.indexOf('return mix(previousTone, currentTone, uTextureMix)')
|
||||||
|
const diffuseStart = shader.indexOf('if (frosted > 0.5 && usesPrefilteredFrost <= 0.5)')
|
||||||
|
|
||||||
|
expect(shader).toContain('bool needsPrevious = uTextureMix < 0.999')
|
||||||
|
expect(shader).toContain('bool needsCurrent = uTextureMix > 0.001')
|
||||||
|
expect(shader).toContain('if (needsPrevious)')
|
||||||
|
expect(shader).toContain('if (needsCurrent)')
|
||||||
|
expect(previousToneIndex).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(currentToneIndex).toBeGreaterThan(previousToneIndex)
|
||||||
|
expect(mixIndex).toBeGreaterThan(currentToneIndex)
|
||||||
|
expect(diffuseStart).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(shader.indexOf('sampleHighQualityDiffuse', diffuseStart)).toBeGreaterThan(diffuseStart)
|
||||||
|
expect(shader.indexOf('sampleBalancedDiffuse', diffuseStart)).toBeGreaterThan(diffuseStart)
|
||||||
|
expect(material.uniforms.uQuality.value).toBe(1)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
it('retains a prepared transaction until it becomes the rendered active wallpaper', async () => {
|
it('retains a prepared transaction until it becomes the rendered active wallpaper', async () => {
|
||||||
const three = await import('three')
|
const three = await import('three')
|
||||||
const wallpaperUrl = ref('https://example.com/wallpaper-1.jpg')
|
const wallpaperUrl = ref('https://example.com/wallpaper-1.jpg')
|
||||||
@@ -2768,6 +2860,7 @@ describe('glass optical surface discovery', () => {
|
|||||||
|
|
||||||
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
const dispose = vi.spyOn(three.WebGLRenderer.prototype, 'dispose')
|
const dispose = vi.spyOn(three.WebGLRenderer.prototype, 'dispose')
|
||||||
|
const contextLoss = vi.spyOn(three.WebGLRenderer.prototype, 'forceContextLoss')
|
||||||
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
|
|
||||||
@@ -2775,12 +2868,46 @@ describe('glass optical surface discovery', () => {
|
|||||||
document.dispatchEvent(new Event('visibilitychange'))
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
await vi.advanceTimersByTimeAsync(APP_ACTIVITY_SUSPEND_DELAY_MS - 1)
|
await vi.advanceTimersByTimeAsync(APP_ACTIVITY_SUSPEND_DELAY_MS - 1)
|
||||||
expect(dispose).not.toHaveBeenCalled()
|
expect(dispose).not.toHaveBeenCalled()
|
||||||
|
expect(contextLoss).not.toHaveBeenCalled()
|
||||||
|
|
||||||
visibilityState = 'visible'
|
visibilityState = 'visible'
|
||||||
window.dispatchEvent(new Event('focus'))
|
window.dispatchEvent(new Event('focus'))
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await Promise.resolve()
|
await Promise.resolve()
|
||||||
expect(dispose).not.toHaveBeenCalled()
|
expect(dispose).not.toHaveBeenCalled()
|
||||||
|
expect(contextLoss).not.toHaveBeenCalled()
|
||||||
|
expect(render).toHaveBeenCalled()
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps an initially unfocused visible renderer ready without drawing until focus resumes it', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const visibilityState: DocumentVisibilityState = 'visible'
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||||
|
documentHasFocus = false
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
quality: ref('balanced'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
expect(render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
documentHasFocus = true
|
||||||
|
window.dispatchEvent(new Event('focus'))
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
expect(render).toHaveBeenCalled()
|
expect(render).toHaveBeenCalled()
|
||||||
scope.stop()
|
scope.stop()
|
||||||
})
|
})
|
||||||
@@ -2904,6 +3031,413 @@ describe('glass optical surface discovery', () => {
|
|||||||
scope.stop()
|
scope.stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each(['blur', 'hidden'] as const)(
|
||||||
|
'blocks render, interaction, geometry, and scroll work after a real %s pause',
|
||||||
|
async pauseEvent => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const surface = appendOpticalSurface('app-hover-lift-card', { height: 320, width: 520, x: 40, y: 80 })
|
||||||
|
const root = document.createElement('div')
|
||||||
|
root.append(canvas)
|
||||||
|
Object.defineProperty(root, 'scrollHeight', { configurable: true, value: 1200 })
|
||||||
|
document.body.append(root)
|
||||||
|
let visibilityState: DocumentVisibilityState = 'visible'
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||||
|
vi.stubGlobal('MutationObserver', MutationObserverTriggerMock)
|
||||||
|
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))
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const reflectionStrength = ref(40)
|
||||||
|
const motionRevision = ref(0)
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('fluid'),
|
||||||
|
pageMotion: {
|
||||||
|
acknowledgeGeometryReady: vi.fn(() => true),
|
||||||
|
active: ref(false),
|
||||||
|
epoch: ref(1),
|
||||||
|
opacity: ref(1),
|
||||||
|
revision: motionRevision,
|
||||||
|
},
|
||||||
|
quality: ref('balanced'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
reflectionStrength,
|
||||||
|
surfaceSpace: 'scroll',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
flushQueuedAnimationFrames(callbacks)
|
||||||
|
render.mockClear()
|
||||||
|
callbacks.clear()
|
||||||
|
const framesBeforePause = renderer?.renderedFrames.value
|
||||||
|
|
||||||
|
if (pauseEvent === 'blur') {
|
||||||
|
documentHasFocus = false
|
||||||
|
window.dispatchEvent(new Event('blur'))
|
||||||
|
} else {
|
||||||
|
visibilityState = 'hidden'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(callbacks.size).toBe(0)
|
||||||
|
|
||||||
|
setOpticalSurfaceBounds(surface, { height: 360, width: 560, x: 220, y: 180 })
|
||||||
|
reflectionStrength.value = 100
|
||||||
|
motionRevision.value += 1
|
||||||
|
ResizeObserverMock.instances.forEach(observer => observer.trigger())
|
||||||
|
MutationObserverTriggerMock.instances[0]?.trigger([createAttributeRecord(surface)])
|
||||||
|
window.dispatchEvent(new MouseEvent('pointermove', { clientX: 280, clientY: 240 }))
|
||||||
|
window.dispatchEvent(new Event('scroll'))
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(render).not.toHaveBeenCalled()
|
||||||
|
expect(renderer?.renderedFrames.value).toBe(framesBeforePause)
|
||||||
|
expect(callbacks.size).toBe(0)
|
||||||
|
scope.stop()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
it('restores the latest uniform and scroll geometry after a paused resume', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const root = document.createElement('div')
|
||||||
|
root.append(canvas)
|
||||||
|
Object.defineProperty(root, 'scrollHeight', { configurable: true, value: 1200 })
|
||||||
|
Object.defineProperty(root, 'scrollWidth', { configurable: true, value: 1200 })
|
||||||
|
document.body.append(root)
|
||||||
|
const surface = appendOpticalSurface('app-hover-lift-card', { height: 320, width: 520, x: 40, y: 80 })
|
||||||
|
let visibilityState: DocumentVisibilityState = 'visible'
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||||
|
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))
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const reflectionStrength = ref(40)
|
||||||
|
const motionRevision = ref(0)
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('off'),
|
||||||
|
pageMotion: {
|
||||||
|
acknowledgeGeometryReady: vi.fn(() => true),
|
||||||
|
active: ref(false),
|
||||||
|
epoch: ref(1),
|
||||||
|
opacity: ref(1),
|
||||||
|
revision: motionRevision,
|
||||||
|
},
|
||||||
|
quality: ref('balanced'),
|
||||||
|
reflectionStrength,
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
surfaceSpace: 'scroll',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
flushQueuedAnimationFrames(callbacks)
|
||||||
|
const scene = render.mock.calls
|
||||||
|
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
|
||||||
|
.find(candidate => candidate.children[0]?.material?.uniforms.uRects)
|
||||||
|
if (!scene) throw new Error('main optical scene was not rendered')
|
||||||
|
const uniforms = scene.children[0].material!.uniforms
|
||||||
|
render.mockClear()
|
||||||
|
callbacks.clear()
|
||||||
|
|
||||||
|
visibilityState = 'hidden'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
setOpticalSurfaceBounds(surface, { height: 360, width: 560, x: 220, y: 180 })
|
||||||
|
reflectionStrength.value = 100
|
||||||
|
motionRevision.value += 1
|
||||||
|
ResizeObserverMock.instances.forEach(observer => observer.trigger())
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(render).not.toHaveBeenCalled()
|
||||||
|
expect(callbacks.size).toBe(0)
|
||||||
|
|
||||||
|
visibilityState = 'visible'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
window.dispatchEvent(new Event('focus'))
|
||||||
|
window.dispatchEvent(new Event('pageshow'))
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
expect(render).toHaveBeenCalledTimes(1)
|
||||||
|
expect(uniforms.uReflectionStrength.value).toBeGreaterThan(1)
|
||||||
|
expect(uniforms.uRects.value[0].x).toBeCloseTo(220 / 1200)
|
||||||
|
expect(uniforms.uRects.value[0].y).toBeCloseTo(1 - (180 + 360) / 1200)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels a pending resume when blur arrives before nextTick and resumes on the next focus', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
let visibilityState: DocumentVisibilityState = 'visible'
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
quality: ref('balanced'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
render.mockClear()
|
||||||
|
visibilityState = 'hidden'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
visibilityState = 'visible'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
documentHasFocus = false
|
||||||
|
window.dispatchEvent(new Event('blur'))
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
expect(render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
documentHasFocus = true
|
||||||
|
window.dispatchEvent(new Event('focus'))
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(render).toHaveBeenCalledTimes(1)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps hidden-to-visible rendering paused without focus until an explicit focus event', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
let visibilityState: DocumentVisibilityState = 'visible'
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
quality: ref('balanced'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
render.mockClear()
|
||||||
|
visibilityState = 'hidden'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
documentHasFocus = false
|
||||||
|
visibilityState = 'visible'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
window.dispatchEvent(new Event('pageshow'))
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
expect(render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
documentHasFocus = true
|
||||||
|
window.dispatchEvent(new Event('focus'))
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(render).toHaveBeenCalledTimes(1)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('commits correct native-scroll pixels before releasing the takeover on resume', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const root = document.createElement('div')
|
||||||
|
root.append(canvas)
|
||||||
|
Object.defineProperty(root, 'scrollHeight', { configurable: true, value: 1200 })
|
||||||
|
document.body.append(root)
|
||||||
|
appendOpticalSurface('app-hover-lift-card', { height: 320, width: 520, x: 40, y: 80 })
|
||||||
|
let visibilityState: DocumentVisibilityState = 'visible'
|
||||||
|
let scrollY = 0
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||||
|
vi.spyOn(window, 'scrollY', 'get').mockImplementation(() => scrollY)
|
||||||
|
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))
|
||||||
|
const snapshots: Array<{ hasWallpaper: number; nativePresentation: string | undefined; scrollOffset: number }> = []
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render').mockImplementation(scene => {
|
||||||
|
const material = (scene as unknown as { children: Array<{ material?: ShaderMaterial }> }).children[0]?.material
|
||||||
|
const uniforms = material?.uniforms as
|
||||||
|
{ uHasWallpaperTexture?: { value: number }; uScrollOffset?: { value: { y: number } } } | undefined
|
||||||
|
if (uniforms?.uHasWallpaperTexture && uniforms.uScrollOffset) {
|
||||||
|
snapshots.push({
|
||||||
|
hasWallpaper: uniforms.uHasWallpaperTexture.value,
|
||||||
|
nativePresentation: document.documentElement.dataset.glassScrollPresentation,
|
||||||
|
scrollOffset: uniforms.uScrollOffset.value.y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('off'),
|
||||||
|
quality: ref('balanced'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
surfaceSpace: 'scroll',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
flushQueuedAnimationFrames(callbacks)
|
||||||
|
const scene = render.mock.calls.at(-1)?.[0] as unknown as {
|
||||||
|
children: Array<{
|
||||||
|
material: { uniforms: { uHasWallpaperTexture: { value: number }; uScrollOffset: { value: { y: number } } } }
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
const uniforms = scene.children[0].material.uniforms
|
||||||
|
snapshots.length = 0
|
||||||
|
render.mockClear()
|
||||||
|
|
||||||
|
scrollY = 240
|
||||||
|
window.dispatchEvent(new Event('scroll'))
|
||||||
|
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
|
||||||
|
expect(uniforms.uHasWallpaperTexture.value).toBe(0)
|
||||||
|
const firstScrollFrame = [...callbacks.values()][0]
|
||||||
|
callbacks.clear()
|
||||||
|
firstScrollFrame?.(performance.now() + 16)
|
||||||
|
expect(uniforms.uScrollOffset.value.y).toBe(240)
|
||||||
|
|
||||||
|
visibilityState = 'hidden'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
expect(callbacks.size).toBe(0)
|
||||||
|
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
|
||||||
|
expect(uniforms.uHasWallpaperTexture.value).toBe(0)
|
||||||
|
|
||||||
|
scrollY = 480
|
||||||
|
window.dispatchEvent(new Event('scroll'))
|
||||||
|
expect(uniforms.uScrollOffset.value.y).toBe(240)
|
||||||
|
|
||||||
|
visibilityState = 'visible'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
expect(snapshots).toEqual([{ hasWallpaper: 1, nativePresentation: 'native', scrollOffset: 480 }])
|
||||||
|
expect(document.documentElement.dataset.glassScrollPresentation).toBeUndefined()
|
||||||
|
expect(uniforms.uHasWallpaperTexture.value).toBe(1)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('waits for page-motion geometry acknowledgement after a paused resume', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const root = document.createElement('div')
|
||||||
|
root.append(canvas)
|
||||||
|
Object.defineProperty(root, 'scrollHeight', { configurable: true, value: 1200 })
|
||||||
|
document.body.append(root)
|
||||||
|
appendOpticalSurface('app-hover-lift-card', { height: 320, width: 520, x: 40, y: 80 })
|
||||||
|
let visibilityState: DocumentVisibilityState = 'visible'
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||||
|
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))
|
||||||
|
const acknowledgeGeometryReady = vi.fn(() => true)
|
||||||
|
const motionActive = ref(false)
|
||||||
|
const motionEpoch = ref(0)
|
||||||
|
const motionRevision = ref(0)
|
||||||
|
const committedWeights: number[] = []
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render').mockImplementation(scene => {
|
||||||
|
const material = (scene as unknown as { children: Array<{ material?: ShaderMaterial }> }).children[0]?.material
|
||||||
|
const weights = material?.uniforms.uSurfaceWeights?.value as number[] | undefined
|
||||||
|
if (weights) committedWeights.push(weights[0])
|
||||||
|
})
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('off'),
|
||||||
|
pageMotion: {
|
||||||
|
acknowledgeGeometryReady,
|
||||||
|
active: motionActive,
|
||||||
|
epoch: motionEpoch,
|
||||||
|
opacity: ref(1),
|
||||||
|
revision: motionRevision,
|
||||||
|
},
|
||||||
|
quality: ref('balanced'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
surfaceSpace: 'scroll',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
flushQueuedAnimationFrames(callbacks)
|
||||||
|
committedWeights.length = 0
|
||||||
|
render.mockClear()
|
||||||
|
callbacks.clear()
|
||||||
|
|
||||||
|
motionEpoch.value = 7
|
||||||
|
motionActive.value = true
|
||||||
|
expect(callbacks.size).toBe(1)
|
||||||
|
visibilityState = 'hidden'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
expect(callbacks.size).toBe(0)
|
||||||
|
expect(acknowledgeGeometryReady).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
visibilityState = 'visible'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
await nextTick()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
expect(acknowledgeGeometryReady).not.toHaveBeenCalled()
|
||||||
|
expect(committedWeights[0]).toBe(0)
|
||||||
|
flushQueuedAnimationFrames(callbacks, 6)
|
||||||
|
|
||||||
|
expect(acknowledgeGeometryReady).toHaveBeenCalledOnce()
|
||||||
|
expect(acknowledgeGeometryReady).toHaveBeenCalledWith(7, expect.any(Number))
|
||||||
|
expect(committedWeights.at(-1)).toBe(1)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
it('releases renderer resources after the long background timeout', async () => {
|
it('releases renderer resources after the long background timeout', async () => {
|
||||||
const three = await import('three')
|
const three = await import('three')
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
@@ -2945,11 +3479,100 @@ describe('glass optical surface discovery', () => {
|
|||||||
scope.stop()
|
scope.stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('rebuilds long-background resources and acknowledges the latest page-motion epoch', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const root = document.createElement('div')
|
||||||
|
root.append(canvas)
|
||||||
|
Object.defineProperty(root, 'scrollHeight', { configurable: true, value: 1200 })
|
||||||
|
document.body.append(root)
|
||||||
|
appendOpticalSurface('app-hover-lift-card', { height: 320, width: 520, x: 40, y: 80 })
|
||||||
|
let visibilityState: DocumentVisibilityState = 'visible'
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||||
|
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))
|
||||||
|
const acknowledgeGeometryReady = vi.fn()
|
||||||
|
const committedWeights: number[] = []
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render').mockImplementation(scene => {
|
||||||
|
const material = (scene as unknown as { children: Array<{ material?: ShaderMaterial }> }).children[0]?.material
|
||||||
|
const weights = material?.uniforms.uSurfaceWeights?.value as number[] | undefined
|
||||||
|
if (weights) committedWeights.push(weights[0])
|
||||||
|
})
|
||||||
|
const motionActive = ref(false)
|
||||||
|
const motionEpoch = ref(0)
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('off'),
|
||||||
|
pageMotion: {
|
||||||
|
acknowledgeGeometryReady,
|
||||||
|
active: motionActive,
|
||||||
|
epoch: motionEpoch,
|
||||||
|
opacity: ref(1),
|
||||||
|
revision: ref(0),
|
||||||
|
},
|
||||||
|
quality: ref('balanced'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
surfaceSpace: 'scroll',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
flushQueuedAnimationFrames(callbacks)
|
||||||
|
committedWeights.length = 0
|
||||||
|
render.mockClear()
|
||||||
|
callbacks.clear()
|
||||||
|
|
||||||
|
motionEpoch.value = 11
|
||||||
|
motionActive.value = true
|
||||||
|
motionEpoch.value = 12
|
||||||
|
expect(callbacks.size).toBe(1)
|
||||||
|
|
||||||
|
const dispose = vi.spyOn(three.WebGLRenderer.prototype, 'dispose')
|
||||||
|
const contextLoss = vi.spyOn(three.WebGLRenderer.prototype, 'forceContextLoss')
|
||||||
|
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
||||||
|
visibilityState = 'hidden'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
expect(callbacks.size).toBe(0)
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(APP_ACTIVITY_SUSPEND_DELAY_MS)
|
||||||
|
expect(dispose).toHaveBeenCalledTimes(1)
|
||||||
|
expect(contextLoss).not.toHaveBeenCalled()
|
||||||
|
expect(acknowledgeGeometryReady).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
visibilityState = 'visible'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
window.dispatchEvent(new Event('focus'))
|
||||||
|
await nextTick()
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
|
||||||
|
expect(acknowledgeGeometryReady).not.toHaveBeenCalled()
|
||||||
|
expect(callbacks.size).toBeGreaterThan(0)
|
||||||
|
flushQueuedAnimationFrames(callbacks, 8)
|
||||||
|
|
||||||
|
expect(acknowledgeGeometryReady).toHaveBeenCalledOnce()
|
||||||
|
expect(acknowledgeGeometryReady).toHaveBeenCalledWith(12, expect.any(Number))
|
||||||
|
expect(committedWeights).toContain(0)
|
||||||
|
expect(committedWeights.at(-1)).toBe(1)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
it('releases a paused renderer after the visible window remains unfocused', async () => {
|
it('releases a paused renderer after the visible window remains unfocused', async () => {
|
||||||
const three = await import('three')
|
const three = await import('three')
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible')
|
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible')
|
||||||
vi.spyOn(document, 'hasFocus').mockReturnValue(false)
|
documentHasFocus = false
|
||||||
const scope = effectScope()
|
const scope = effectScope()
|
||||||
const renderer = scope.run(() =>
|
const renderer = scope.run(() =>
|
||||||
useGlassOpticalRenderer({
|
useGlassOpticalRenderer({
|
||||||
|
|||||||
@@ -707,27 +707,34 @@ vec3 toneMapWallpaper(vec3 color, vec2 uv, float wallpaperExposure) {
|
|||||||
vec3 sampleWallpaper(vec2 uv) {
|
vec3 sampleWallpaper(vec2 uv) {
|
||||||
vec2 viewportUv = vec2(0.5) + (uv - vec2(0.5)) / max(uCoverScale, vec2(0.0001));
|
vec2 viewportUv = vec2(0.5) + (uv - vec2(0.5)) / max(uCoverScale, vec2(0.0001));
|
||||||
vec2 previousUv = vec2(0.5) + (viewportUv - vec2(0.5)) * uPreviousCoverScale;
|
vec2 previousUv = vec2(0.5) + (viewportUv - vec2(0.5)) * uPreviousCoverScale;
|
||||||
vec3 previous;
|
vec3 previous = vec3(0.0);
|
||||||
vec3 current;
|
vec3 current = vec3(0.0);
|
||||||
|
// 稳定端点只读取参与输出的纹理;过渡中仍按各自曝光映射后混合。
|
||||||
|
bool needsPrevious = uTextureMix < 0.999;
|
||||||
|
bool needsCurrent = uTextureMix > 0.001;
|
||||||
if (uAppearance > 1.5 && uHasFrostedTexture > 0.5) {
|
if (uAppearance > 1.5 && uHasFrostedTexture > 0.5) {
|
||||||
float frostLod = (1.0 - uFrostDetailLevel) * 6.0;
|
float frostLod = (1.0 - uFrostDetailLevel) * 6.0;
|
||||||
// 低分辨率预滤已经扩大了每个 texel 的原图 footprint,LOD 只追加当前纹理内的低通层级。
|
// 低分辨率预滤已经扩大了每个 texel 的原图 footprint,LOD 只追加当前纹理内的低通层级。
|
||||||
float frostGradientScale = exp2(frostLod);
|
float frostGradientScale = exp2(frostLod);
|
||||||
|
if (needsPrevious) {
|
||||||
previous = texture2DGradEXT(
|
previous = texture2DGradEXT(
|
||||||
uPreviousFrostedTexture,
|
uPreviousFrostedTexture,
|
||||||
previousUv,
|
previousUv,
|
||||||
dFdx(previousUv) * frostGradientScale,
|
dFdx(previousUv) * frostGradientScale,
|
||||||
dFdy(previousUv) * frostGradientScale
|
dFdy(previousUv) * frostGradientScale
|
||||||
).rgb;
|
).rgb;
|
||||||
|
}
|
||||||
|
if (needsCurrent) {
|
||||||
current = texture2DGradEXT(
|
current = texture2DGradEXT(
|
||||||
uFrostedTexture,
|
uFrostedTexture,
|
||||||
uv,
|
uv,
|
||||||
dFdx(uv) * frostGradientScale,
|
dFdx(uv) * frostGradientScale,
|
||||||
dFdy(uv) * frostGradientScale
|
dFdy(uv) * frostGradientScale
|
||||||
).rgb;
|
).rgb;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
previous = texture2D(uPreviousTexture, previousUv).rgb;
|
if (needsPrevious) previous = texture2D(uPreviousTexture, previousUv).rgb;
|
||||||
current = texture2D(uTexture, uv).rgb;
|
if (needsCurrent) current = texture2D(uTexture, uv).rgb;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uTextureMix <= 0.001) {
|
if (uTextureMix <= 0.001) {
|
||||||
@@ -929,6 +936,8 @@ ${GLASS_FLUID_FRAGMENT_SURFACE_REFRACTION}
|
|||||||
? refracted
|
? refracted
|
||||||
: sampleChromatic(sourceUv, detailSeparation);
|
: sampleChromatic(sourceUv, detailSeparation);
|
||||||
refracted = mix(refracted, detailed, mix(0.06, 0.16, uQuality) * (1.0 - frosted));
|
refracted = mix(refracted, detailed, mix(0.06, 0.16, uQuality) * (1.0 - frosted));
|
||||||
|
// 非磨砂不使用扩散结果;预滤磨砂已具备低通纹理,两者都无需额外邻域采样。
|
||||||
|
if (frosted > 0.5 && usesPrefilteredFrost <= 0.5) {
|
||||||
vec2 diffusionAxis = length(refraction) > 0.00001 ? normalize(refraction) : wakePerpendicular;
|
vec2 diffusionAxis = length(refraction) > 0.00001 ? normalize(refraction) : wakePerpendicular;
|
||||||
float diffusionRadius =
|
float diffusionRadius =
|
||||||
mix(0.0022, 0.0038, uQuality) *
|
mix(0.0022, 0.0038, uQuality) *
|
||||||
@@ -939,15 +948,11 @@ ${GLASS_FLUID_FRAGMENT_SURFACE_REFRACTION}
|
|||||||
);
|
);
|
||||||
float frostedDensity = frosted * (1.0 - uFrostDetailLevel);
|
float frostedDensity = frosted * (1.0 - uFrostDetailLevel);
|
||||||
diffusionRadius *= 1.0 + frostedDensity * mix(1.15, 1.55, uQuality);
|
diffusionRadius *= 1.0 + frostedDensity * mix(1.15, 1.55, uQuality);
|
||||||
vec3 diffused;
|
vec3 diffused = uQuality > 0.5
|
||||||
if (usesPrefilteredFrost > 0.5) {
|
? sampleHighQualityDiffuse(sourceUv, diffusionAxis, diffusionRadius)
|
||||||
diffused = refracted;
|
: sampleBalancedDiffuse(sourceUv, diffusionAxis, diffusionRadius);
|
||||||
} else if (uQuality > 0.5) {
|
|
||||||
diffused = sampleHighQualityDiffuse(sourceUv, diffusionAxis, diffusionRadius);
|
|
||||||
} else {
|
|
||||||
diffused = sampleBalancedDiffuse(sourceUv, diffusionAxis, diffusionRadius);
|
|
||||||
}
|
|
||||||
refracted = mix(refracted, diffused, frosted);
|
refracted = mix(refracted, diffused, frosted);
|
||||||
|
}
|
||||||
float refractedLuminance = dot(refracted, vec3(0.2126, 0.7152, 0.0722));
|
float refractedLuminance = dot(refracted, vec3(0.2126, 0.7152, 0.0722));
|
||||||
float tinted = step(0.5, uAppearance) * (1.0 - step(1.5, uAppearance));
|
float tinted = step(0.5, uAppearance) * (1.0 - step(1.5, uAppearance));
|
||||||
float transmissionOffset = min(uTransmissionStrength - 1.0, 0.0);
|
float transmissionOffset = min(uTransmissionStrength - 1.0, 0.0);
|
||||||
@@ -1326,12 +1331,19 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
let contextRecoveryPending = false
|
let contextRecoveryPending = false
|
||||||
let resumePromise: Promise<void> | null = null
|
let resumePromise: Promise<void> | null = null
|
||||||
let resumeVersion = 0
|
let resumeVersion = 0
|
||||||
|
// 失焦后的暂停状态由活动事件解除,观察器与参数更新不能自行恢复呈现。
|
||||||
|
let presentationPaused = document.visibilityState === 'hidden' || !document.hasFocus()
|
||||||
let dynamicsGeneration = 0
|
let dynamicsGeneration = 0
|
||||||
const presentationSpace = options.surfaceSpace ?? 'fixed'
|
const presentationSpace = options.surfaceSpace ?? 'fixed'
|
||||||
const usesDynamicsOnly = () =>
|
const usesDynamicsOnly = () =>
|
||||||
presentationSpace === 'scroll' || (presentationSpace === 'fixed' && toValue(options.appearance) === 'frosted')
|
presentationSpace === 'scroll' || (presentationSpace === 'fixed' && toValue(options.appearance) === 'frosted')
|
||||||
const wallpaperSourceCache = options.wallpaperSourceCache ?? createGlassWallpaperSourceCache()
|
const wallpaperSourceCache = options.wallpaperSourceCache ?? createGlassWallpaperSourceCache()
|
||||||
|
|
||||||
|
/** 所有持续绘制入口共享活动边界,资源准备和 uniform 同步不依赖呈现帧。 */
|
||||||
|
function canPresentFrame() {
|
||||||
|
return toValue(options.active) && !presentationPaused && document.visibilityState !== 'hidden'
|
||||||
|
}
|
||||||
|
|
||||||
/** 滚动期间由原生 backdrop 接管壁纸;稳定态恢复完整纹理折射与流体反馈。 */
|
/** 滚动期间由原生 backdrop 接管壁纸;稳定态恢复完整纹理折射与流体反馈。 */
|
||||||
function syncWallpaperSamplingMode() {
|
function syncWallpaperSamplingMode() {
|
||||||
if (!resources) return
|
if (!resources) return
|
||||||
@@ -1347,18 +1359,18 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
scrollPresentationRestoreTimer = null
|
scrollPresentationRestoreTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishNativeScrollPresentation(timestamp = performance.now()) {
|
function finishNativeScrollPresentation(timestamp = performance.now(), advanceFlow = false) {
|
||||||
clearScrollPresentationRestoreTimer()
|
clearScrollPresentationRestoreTimer()
|
||||||
if (presentationSpace !== 'scroll' || !scrollWallpaperSamplingSuppressed) return
|
if (presentationSpace !== 'scroll' || !scrollWallpaperSamplingSuppressed || !canPresentFrame()) return
|
||||||
|
|
||||||
scrollWallpaperSamplingSuppressed = false
|
scrollWallpaperSamplingSuppressed = false
|
||||||
syncWallpaperSamplingMode()
|
syncWallpaperSamplingMode()
|
||||||
renderFrame(timestamp, false)
|
renderFrame(timestamp, advanceFlow)
|
||||||
document.documentElement.removeAttribute('data-glass-scroll-presentation')
|
document.documentElement.removeAttribute('data-glass-scroll-presentation')
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginNativeScrollPresentation() {
|
function beginNativeScrollPresentation() {
|
||||||
if (presentationSpace !== 'scroll' || !resources) return
|
if (presentationSpace !== 'scroll' || !resources || !canPresentFrame()) return
|
||||||
|
|
||||||
clearScrollPresentationRestoreTimer()
|
clearScrollPresentationRestoreTimer()
|
||||||
scrollPresentationRestoreTimer = window.setTimeout(() => finishNativeScrollPresentation(), 180)
|
scrollPresentationRestoreTimer = window.setTimeout(() => finishNativeScrollPresentation(), 180)
|
||||||
@@ -1532,6 +1544,18 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
transformingSurfaces.clear()
|
transformingSurfaces.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 暂停和销毁共用几何帧清理,恢复时按最新 DOM 重新测量。 */
|
||||||
|
function cancelSurfaceUpdateFrames() {
|
||||||
|
if (surfaceUpdateFrame !== null) cancelAnimationFrame(surfaceUpdateFrame)
|
||||||
|
surfaceUpdateFrame = null
|
||||||
|
if (surfaceStabilityFrame !== null) cancelAnimationFrame(surfaceStabilityFrame)
|
||||||
|
surfaceStabilityFrame = null
|
||||||
|
if (presentationResizeTimer !== null) window.clearTimeout(presentationResizeTimer)
|
||||||
|
presentationResizeTimer = null
|
||||||
|
presentationResizeCandidate = ''
|
||||||
|
presentationResizeStableSamples = 0
|
||||||
|
}
|
||||||
|
|
||||||
function clearBackgroundDisposeTimer() {
|
function clearBackgroundDisposeTimer() {
|
||||||
if (backgroundDisposeTimer === null) return
|
if (backgroundDisposeTimer === null) return
|
||||||
|
|
||||||
@@ -1584,7 +1608,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
function renderWallpaperTransitionFrame(timestamp: number) {
|
function renderWallpaperTransitionFrame(timestamp: number) {
|
||||||
wallpaperTransitionFrame = null
|
wallpaperTransitionFrame = null
|
||||||
if (document.visibilityState === 'hidden') return
|
if (!canPresentFrame()) return
|
||||||
|
|
||||||
renderFrame(timestamp)
|
renderFrame(timestamp)
|
||||||
if (previousTexture && wallpaperTransitionFrame === null) {
|
if (previousTexture && wallpaperTransitionFrame === null) {
|
||||||
@@ -1593,7 +1617,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleWallpaperTransition() {
|
function scheduleWallpaperTransition() {
|
||||||
if (wallpaperTransitionFrame !== null || !previousTexture || document.visibilityState === 'hidden') return
|
if (wallpaperTransitionFrame !== null || !previousTexture || !canPresentFrame()) return
|
||||||
|
|
||||||
wallpaperTransitionFrame = requestAnimationFrame(renderWallpaperTransitionFrame)
|
wallpaperTransitionFrame = requestAnimationFrame(renderWallpaperTransitionFrame)
|
||||||
}
|
}
|
||||||
@@ -1821,7 +1845,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderFrame(timestamp = performance.now(), advanceFlow = true) {
|
function renderFrame(timestamp = performance.now(), advanceFlow = true) {
|
||||||
if (!resources || !toValue(options.active) || document.visibilityState === 'hidden') return
|
if (!resources || !canPresentFrame()) return
|
||||||
|
|
||||||
updateWallpaperTransition(timestamp)
|
updateWallpaperTransition(timestamp)
|
||||||
if (fluidDynamics && advanceFlow) {
|
if (fluidDynamics && advanceFlow) {
|
||||||
@@ -1859,7 +1883,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleFrame() {
|
function scheduleFrame() {
|
||||||
if (animationFrame !== null || !resources) return
|
if (animationFrame !== null || !resources || !canPresentFrame()) return
|
||||||
|
|
||||||
animationFrame = requestAnimationFrame(renderScheduledFrame)
|
animationFrame = requestAnimationFrame(renderScheduledFrame)
|
||||||
}
|
}
|
||||||
@@ -2141,11 +2165,13 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleSurfaceUpdate() {
|
function scheduleSurfaceUpdate() {
|
||||||
|
if (!canPresentFrame()) return
|
||||||
if (queueScrollGeometryRefresh(false)) return
|
if (queueScrollGeometryRefresh(false)) return
|
||||||
if (surfaceUpdateFrame !== null || !resources) return
|
if (surfaceUpdateFrame !== null || !resources) return
|
||||||
|
|
||||||
surfaceUpdateFrame = requestAnimationFrame(timestamp => {
|
surfaceUpdateFrame = requestAnimationFrame(timestamp => {
|
||||||
surfaceUpdateFrame = null
|
surfaceUpdateFrame = null
|
||||||
|
if (!canPresentFrame()) return
|
||||||
updateSurfaceUniforms(timestamp, false)
|
updateSurfaceUniforms(timestamp, false)
|
||||||
// 表面失效必须在同一有界帧内清除旧像素,不能等待下一次指针或壁纸事件。
|
// 表面失效必须在同一有界帧内清除旧像素,不能等待下一次指针或壁纸事件。
|
||||||
renderFrame(timestamp, false)
|
renderFrame(timestamp, false)
|
||||||
@@ -2155,6 +2181,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
/** DOM 重排后连续采样少量帧,避免把虚拟列表的中间几何误认为最终表面。 */
|
/** DOM 重排后连续采样少量帧,避免把虚拟列表的中间几何误认为最终表面。 */
|
||||||
function scheduleSurfaceStabilityUpdate(motionEpoch?: number) {
|
function scheduleSurfaceStabilityUpdate(motionEpoch?: number) {
|
||||||
if (motionEpoch !== undefined) pagePresentationMotionEpoch = motionEpoch
|
if (motionEpoch !== undefined) pagePresentationMotionEpoch = motionEpoch
|
||||||
|
if (!canPresentFrame()) return
|
||||||
if (queueScrollGeometryRefresh(true)) return
|
if (queueScrollGeometryRefresh(true)) return
|
||||||
surfaceStabilityPass = 0
|
surfaceStabilityPass = 0
|
||||||
surfaceStableFrameCount = 0
|
surfaceStableFrameCount = 0
|
||||||
@@ -2163,7 +2190,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
const sample = (timestamp: number) => {
|
const sample = (timestamp: number) => {
|
||||||
surfaceStabilityFrame = null
|
surfaceStabilityFrame = null
|
||||||
if (!resources || !toValue(options.active) || document.visibilityState === 'hidden') return
|
if (!resources || !canPresentFrame()) return
|
||||||
|
|
||||||
updateSurfaceUniforms(timestamp, false)
|
updateSurfaceUniforms(timestamp, false)
|
||||||
const signature = surfaceSlots
|
const signature = surfaceSlots
|
||||||
@@ -2203,13 +2230,14 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
* 连续两个 80ms 样本一致才允许覆盖已提交的 presentation 首帧。
|
* 连续两个 80ms 样本一致才允许覆盖已提交的 presentation 首帧。
|
||||||
*/
|
*/
|
||||||
function schedulePresentationResizeUpdate() {
|
function schedulePresentationResizeUpdate() {
|
||||||
|
if (!canPresentFrame()) return
|
||||||
if (presentationResizeTimer !== null) window.clearTimeout(presentationResizeTimer)
|
if (presentationResizeTimer !== null) window.clearTimeout(presentationResizeTimer)
|
||||||
presentationResizeCandidate = ''
|
presentationResizeCandidate = ''
|
||||||
presentationResizeStableSamples = 0
|
presentationResizeStableSamples = 0
|
||||||
|
|
||||||
const sample = () => {
|
const sample = () => {
|
||||||
presentationResizeTimer = null
|
presentationResizeTimer = null
|
||||||
if (!resources) return
|
if (!resources || !canPresentFrame()) return
|
||||||
|
|
||||||
const presentation = measurePresentationSize()
|
const presentation = measurePresentationSize()
|
||||||
const candidate = `${window.innerWidth},${window.innerHeight},${presentation.width},${presentation.height}`
|
const candidate = `${window.innerWidth},${window.innerHeight},${presentation.width},${presentation.height}`
|
||||||
@@ -2231,7 +2259,13 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
/** 共享页面 motion 活跃时,页面几何变化必须在浏览器绘制前完成一次完整 presentation 提交。 */
|
/** 共享页面 motion 活跃时,页面几何变化必须在浏览器绘制前完成一次完整 presentation 提交。 */
|
||||||
function commitActivePagePresentation(timestamp = performance.now()) {
|
function commitActivePagePresentation(timestamp = performance.now()) {
|
||||||
if (!resources || presentationSpace !== 'scroll' || !toValue(options.pageMotion?.active ?? false)) return false
|
if (
|
||||||
|
!resources ||
|
||||||
|
!canPresentFrame() ||
|
||||||
|
presentationSpace !== 'scroll' ||
|
||||||
|
!toValue(options.pageMotion?.active ?? false)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
if (presentationResizeTimer !== null) window.clearTimeout(presentationResizeTimer)
|
if (presentationResizeTimer !== null) window.clearTimeout(presentationResizeTimer)
|
||||||
presentationResizeTimer = null
|
presentationResizeTimer = null
|
||||||
@@ -2246,7 +2280,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
/** 普通表面尺寸即时更新;页面根尺寸在稳定后覆盖 presentation。 */
|
/** 普通表面尺寸即时更新;页面根尺寸在稳定后覆盖 presentation。 */
|
||||||
function handleSurfaceResize(entries: ResizeObserverEntry[]) {
|
function handleSurfaceResize(entries: ResizeObserverEntry[]) {
|
||||||
if (!resources) return
|
if (!resources || !canPresentFrame()) return
|
||||||
|
|
||||||
const presentationRoot = presentationSpace === 'scroll' ? options.canvas.value?.parentElement : null
|
const presentationRoot = presentationSpace === 'scroll' ? options.canvas.value?.parentElement : null
|
||||||
const presentationChanged = presentationRoot && entries.some(entry => entry.target === presentationRoot)
|
const presentationChanged = presentationRoot && entries.some(entry => entry.target === presentationRoot)
|
||||||
@@ -2262,12 +2296,13 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
/** CSS transform 不改变布局尺寸,过渡期间用有界帧同步真实几何并清除旧蒙版。 */
|
/** CSS transform 不改变布局尺寸,过渡期间用有界帧同步真实几何并清除旧蒙版。 */
|
||||||
function scheduleSurfaceTransformFrame() {
|
function scheduleSurfaceTransformFrame() {
|
||||||
|
if (!canPresentFrame()) return
|
||||||
if (queueScrollGeometryRefresh(false)) return
|
if (queueScrollGeometryRefresh(false)) return
|
||||||
if (surfaceTransformFrame !== null || !resources) return
|
if (surfaceTransformFrame !== null || !resources) return
|
||||||
|
|
||||||
surfaceTransformFrame = requestAnimationFrame(timestamp => {
|
surfaceTransformFrame = requestAnimationFrame(timestamp => {
|
||||||
surfaceTransformFrame = null
|
surfaceTransformFrame = null
|
||||||
if (!resources || !toValue(options.active) || document.visibilityState === 'hidden') {
|
if (!resources || !canPresentFrame()) {
|
||||||
cancelSurfaceTransformFrame()
|
cancelSurfaceTransformFrame()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2455,7 +2490,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resizeRenderer() {
|
function resizeRenderer() {
|
||||||
if (!resources) return
|
if (!resources || !canPresentFrame()) return
|
||||||
|
|
||||||
const viewportWidth = window.innerWidth
|
const viewportWidth = window.innerWidth
|
||||||
const viewportHeight = window.innerHeight
|
const viewportHeight = window.innerHeight
|
||||||
@@ -2660,7 +2695,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderInteractionFrame(timestamp: number) {
|
function renderInteractionFrame(timestamp: number) {
|
||||||
if (!resources || !toValue(options.active) || document.visibilityState === 'hidden') {
|
if (!resources || !canPresentFrame()) {
|
||||||
animationFrame = null
|
animationFrame = null
|
||||||
interactionAnimating = false
|
interactionAnimating = false
|
||||||
return
|
return
|
||||||
@@ -2725,7 +2760,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startInteractionAnimation() {
|
function startInteractionAnimation() {
|
||||||
if (interactionAnimating) return
|
if (interactionAnimating || !canPresentFrame()) return
|
||||||
|
|
||||||
cancelScheduledFrame()
|
cancelScheduledFrame()
|
||||||
interactionAnimating = true
|
interactionAnimating = true
|
||||||
@@ -2742,6 +2777,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
target?: EventTarget | null,
|
target?: EventTarget | null,
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
|
!canPresentFrame() ||
|
||||||
!hasDynamicCapability() ||
|
!hasDynamicCapability() ||
|
||||||
(hasRippleCapability() && presentationSpace === 'scroll' && scrollWallpaperSamplingSuppressed)
|
(hasRippleCapability() && presentationSpace === 'scroll' && scrollWallpaperSamplingSuppressed)
|
||||||
) {
|
) {
|
||||||
@@ -2940,12 +2976,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
scrollAnimationFrame = null
|
scrollAnimationFrame = null
|
||||||
scrollFrameCommitted = false
|
scrollFrameCommitted = false
|
||||||
scrollLateGeometryCommitted = false
|
scrollLateGeometryCommitted = false
|
||||||
if (
|
if (presentationSpace !== 'scroll' || !resources || !canPresentFrame()) {
|
||||||
presentationSpace !== 'scroll' ||
|
|
||||||
!resources ||
|
|
||||||
!toValue(options.active) ||
|
|
||||||
document.visibilityState === 'hidden'
|
|
||||||
) {
|
|
||||||
scrollDirty = false
|
scrollDirty = false
|
||||||
scrollStableFrameCount = 0
|
scrollStableFrameCount = 0
|
||||||
return
|
return
|
||||||
@@ -2991,13 +3022,13 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleScrollFrame() {
|
function scheduleScrollFrame() {
|
||||||
if (scrollAnimationFrame !== null || presentationSpace !== 'scroll' || !resources) return
|
if (scrollAnimationFrame !== null || presentationSpace !== 'scroll' || !resources || !canPresentFrame()) return
|
||||||
|
|
||||||
scrollAnimationFrame = requestAnimationFrame(renderScrollFrame)
|
scrollAnimationFrame = requestAnimationFrame(renderScrollFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScroll(event: Event) {
|
function handleScroll(event: Event) {
|
||||||
if (presentationSpace !== 'scroll' || !resources) return
|
if (presentationSpace !== 'scroll' || !resources || !canPresentFrame()) return
|
||||||
|
|
||||||
const target = event.target
|
const target = event.target
|
||||||
if (!isRelevantScrollTarget(target)) return
|
if (!isRelevantScrollTarget(target)) return
|
||||||
@@ -3034,7 +3065,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleScrollEnd(event: Event) {
|
function handleScrollEnd(event: Event) {
|
||||||
if (presentationSpace !== 'scroll' || !resources) return
|
if (presentationSpace !== 'scroll' || !resources || !canPresentFrame()) return
|
||||||
|
|
||||||
const target = event.target
|
const target = event.target
|
||||||
if (
|
if (
|
||||||
@@ -3052,20 +3083,25 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
/** 暂停事件驱动帧但保留 WebGL context、纹理、流场和最后一张稳定画面。 */
|
/** 暂停事件驱动帧但保留 WebGL context、纹理、流场和最后一张稳定画面。 */
|
||||||
function pauseRenderer() {
|
function pauseRenderer() {
|
||||||
|
presentationPaused = true
|
||||||
|
resumeVersion += 1
|
||||||
|
resumePromise = null
|
||||||
cancelScheduledFrame()
|
cancelScheduledFrame()
|
||||||
cancelScrollFrame()
|
cancelScrollFrame()
|
||||||
finishNativeScrollPresentation()
|
// 原生滚动背板保持接管,恢复时先提交正确像素再揭示 canvas。
|
||||||
cancelWallpaperTransitionFrame()
|
cancelWallpaperTransitionFrame()
|
||||||
cancelSurfaceTransformFrame()
|
cancelSurfaceTransformFrame()
|
||||||
|
cancelSurfaceUpdateFrames()
|
||||||
interactionAnimating = false
|
interactionAnimating = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合并同一可见性事务的多个浏览器事件,只恢复一次稳定帧。 */
|
/** 合并同一可见性事务的多个浏览器事件,只恢复一次稳定帧。 */
|
||||||
function resumeRenderer() {
|
function resumeRenderer() {
|
||||||
|
if (!canPresentFrame()) return Promise.resolve()
|
||||||
if (resumePromise) return resumePromise
|
if (resumePromise) return resumePromise
|
||||||
|
|
||||||
const version = resumeVersion
|
const version = resumeVersion
|
||||||
const canResume = () => toValue(options.active) && document.visibilityState !== 'hidden'
|
const canResume = () => canPresentFrame()
|
||||||
const task = (async () => {
|
const task = (async () => {
|
||||||
clearBackgroundDisposeTimer()
|
clearBackgroundDisposeTimer()
|
||||||
if (!canResume()) return
|
if (!canResume()) return
|
||||||
@@ -3074,6 +3110,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
if (version !== resumeVersion || !canResume()) return
|
if (version !== resumeVersion || !canResume()) return
|
||||||
if (!resources) {
|
if (!resources) {
|
||||||
await initializeRenderer()
|
await initializeRenderer()
|
||||||
|
if (canPresentFrame() && !pagePresentationGeometryReady) scheduleSurfaceStabilityUpdate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3082,12 +3119,14 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
const timestamp = performance.now()
|
const timestamp = performance.now()
|
||||||
const keepRippleAnimating = hasRippleCapability() ? advanceRipple(timestamp) : false
|
const keepRippleAnimating = hasRippleCapability() ? advanceRipple(timestamp) : false
|
||||||
if (!keepRippleAnimating) resetInteractionState()
|
if (!keepRippleAnimating) resetInteractionState()
|
||||||
renderFrame(timestamp, !hasRippleCapability())
|
if (scrollWallpaperSamplingSuppressed) finishNativeScrollPresentation(timestamp, !hasRippleCapability())
|
||||||
|
else renderFrame(timestamp, !hasRippleCapability())
|
||||||
if (keepRippleAnimating) {
|
if (keepRippleAnimating) {
|
||||||
interactionAnimating = true
|
interactionAnimating = true
|
||||||
animationFrame = requestAnimationFrame(renderInteractionFrame)
|
animationFrame = requestAnimationFrame(renderInteractionFrame)
|
||||||
}
|
}
|
||||||
scheduleWallpaperTransition()
|
scheduleWallpaperTransition()
|
||||||
|
if (!pagePresentationGeometryReady) scheduleSurfaceStabilityUpdate()
|
||||||
})()
|
})()
|
||||||
|
|
||||||
resumePromise = task
|
resumePromise = task
|
||||||
@@ -3111,12 +3150,13 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleVisibilityChange() {
|
function handleVisibilityChange() {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (document.visibilityState === 'hidden' || !document.hasFocus()) {
|
||||||
pauseRenderer()
|
pauseRenderer()
|
||||||
scheduleInactiveRendererDisposal(() => document.visibilityState === 'hidden')
|
scheduleInactiveRendererDisposal(() => document.visibilityState === 'hidden' || !document.hasFocus())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
presentationPaused = false
|
||||||
void resumeRenderer()
|
void resumeRenderer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3127,8 +3167,15 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
scheduleInactiveRendererDisposal(() => document.visibilityState === 'visible' && !document.hasFocus())
|
scheduleInactiveRendererDisposal(() => document.visibilityState === 'visible' && !document.hasFocus())
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowResume() {
|
function handleWindowResume(event: Event) {
|
||||||
if (document.visibilityState === 'visible') void resumeRenderer()
|
if (document.visibilityState !== 'visible') return
|
||||||
|
if (event.type === 'pageshow' && !document.hasFocus()) {
|
||||||
|
handleWindowBlur()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
presentationPaused = false
|
||||||
|
void resumeRenderer()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContextLost(event: Event) {
|
function handleContextLost(event: Event) {
|
||||||
@@ -3350,21 +3397,8 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
cancelScrollFrame()
|
cancelScrollFrame()
|
||||||
cancelWallpaperTransitionFrame()
|
cancelWallpaperTransitionFrame()
|
||||||
cancelSurfaceTransformFrame()
|
cancelSurfaceTransformFrame()
|
||||||
|
cancelSurfaceUpdateFrames()
|
||||||
clearBackgroundDisposeTimer()
|
clearBackgroundDisposeTimer()
|
||||||
if (surfaceUpdateFrame !== null) {
|
|
||||||
cancelAnimationFrame(surfaceUpdateFrame)
|
|
||||||
surfaceUpdateFrame = null
|
|
||||||
}
|
|
||||||
if (surfaceStabilityFrame !== null) {
|
|
||||||
cancelAnimationFrame(surfaceStabilityFrame)
|
|
||||||
surfaceStabilityFrame = null
|
|
||||||
}
|
|
||||||
if (presentationResizeTimer !== null) {
|
|
||||||
window.clearTimeout(presentationResizeTimer)
|
|
||||||
presentationResizeTimer = null
|
|
||||||
}
|
|
||||||
presentationResizeCandidate = ''
|
|
||||||
presentationResizeStableSamples = 0
|
|
||||||
removeEvents()
|
removeEvents()
|
||||||
resizeObserver?.disconnect()
|
resizeObserver?.disconnect()
|
||||||
resizeObserver = null
|
resizeObserver = null
|
||||||
@@ -3994,6 +4028,9 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
resizeRenderer()
|
resizeRenderer()
|
||||||
await loadWallpaper(toValue(options.wallpaperUrl), version)
|
await loadWallpaper(toValue(options.wallpaperUrl), version)
|
||||||
preparePendingWallpaper()
|
preparePendingWallpaper()
|
||||||
|
if (version === loadVersion && presentationPaused) {
|
||||||
|
scheduleInactiveRendererDisposal(() => document.visibilityState === 'hidden' || !document.hasFocus())
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fallbackFromCurrentLoad(version, '玻璃光学渲染器初始化失败,已回退标准材质:', error)
|
fallbackFromCurrentLoad(version, '玻璃光学渲染器初始化失败,已回退标准材质:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user