mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-10 16:13:28 +08:00
fix(glass): disable mobile motion dynamics (#623)
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
type GlassOpticalPreset,
|
||||
type GlassOpticalPresetOverrides,
|
||||
} from '@/utils/glassOptics'
|
||||
import { useGlassMobilePresentation } from '@/composables/useGlassPresentationCapabilities'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -38,6 +39,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const display = useDisplay()
|
||||
const usesMobilePresentation = useGlassMobilePresentation()
|
||||
const { settings } = useThemeCustomizer()
|
||||
const draftAppearance = ref<ThemeCustomizerGlassAppearance>(settings.value.glassAppearance)
|
||||
const draftDeformationStrength = ref(settings.value.glassDeformationStrength)
|
||||
@@ -51,7 +53,7 @@ const draftTranslationStrength = ref(settings.value.glassTranslationStrength)
|
||||
const draftTransparencyStrength = ref(settings.value.glassTransparencyStrength)
|
||||
const isSaving = ref(false)
|
||||
const usesRealtimeOptics = computed(() => draftQuality.value !== 'css')
|
||||
const showsDynamicTuning = computed(() => usesRealtimeOptics.value)
|
||||
const showsDynamicTuning = computed(() => usesRealtimeOptics.value && !usesMobilePresentation.value)
|
||||
const availablePresets = computed(() => getAvailableGlassOpticalPresets(draftQuality.value))
|
||||
const activePreset = computed<GlassOpticalPreset>(() =>
|
||||
availablePresets.value.includes(draftPreset.value) ? draftPreset.value : 'natural',
|
||||
@@ -105,7 +107,11 @@ const qualityOptions: Array<{
|
||||
{ hint: 'theme.glassQualityBalancedHint', label: 'theme.glassQualityBalanced', value: 'balanced' },
|
||||
{ hint: 'theme.glassQualityHighHint', label: 'theme.glassQualityHigh', value: 'high' },
|
||||
]
|
||||
const qualityHint = computed(() => qualityOptions.find(option => option.value === draftQuality.value)?.hint ?? '')
|
||||
const qualityHint = computed(() => {
|
||||
if (usesMobilePresentation.value && draftQuality.value !== 'css') return 'theme.glassQualityMobileHint'
|
||||
|
||||
return qualityOptions.find(option => option.value === draftQuality.value)?.hint ?? ''
|
||||
})
|
||||
const presetOptions: Array<{ label: string; value: GlassOpticalPreset }> = [
|
||||
{ label: 'theme.glassPresetNatural', value: 'natural' },
|
||||
{ label: 'theme.glassPresetGlide', value: 'glide' },
|
||||
@@ -410,6 +416,9 @@ onScopeDispose(cancelGlassPreview)
|
||||
thumb-label
|
||||
@update:model-value="updateReflectionStrength"
|
||||
/>
|
||||
<p class="glass-settings-dialog__hint">
|
||||
{{ t('theme.glassMaterialStrengthHint') }}
|
||||
</p>
|
||||
|
||||
<div v-if="showsDynamicTuning" class="glass-settings-dialog__live-controls">
|
||||
<h3 class="glass-settings-dialog__group-label">{{ t('theme.glassDynamicTuning') }}</h3>
|
||||
@@ -463,10 +472,10 @@ onScopeDispose(cancelGlassPreview)
|
||||
thumb-label
|
||||
@update:model-value="updateFlowStrength"
|
||||
/>
|
||||
<p class="glass-settings-dialog__hint">
|
||||
{{ t('theme.glassOpticalStrengthHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="glass-settings-dialog__hint">
|
||||
{{ t(showsDynamicTuning ? 'theme.glassOpticalStrengthHint' : 'theme.glassOpticalStrengthUnavailableHint') }}
|
||||
</p>
|
||||
</section>
|
||||
</VCardText>
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ const mocks = vi.hoisted(() => ({
|
||||
cancelGlassPreview: vi.fn(),
|
||||
commitGlassPreview: vi.fn(),
|
||||
previewGlassSettings: vi.fn(),
|
||||
usesMobilePresentation: null as { value: boolean } | null,
|
||||
display: {
|
||||
smAndDown: { value: false },
|
||||
},
|
||||
settings: {
|
||||
value: {
|
||||
glassAppearance: 'clear',
|
||||
@@ -53,14 +57,25 @@ vi.mock('vue-i18n', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('vuetify', () => ({
|
||||
useDisplay: () => ({ smAndDown: { value: true } }),
|
||||
useDisplay: () => mocks.display,
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useGlassPresentationCapabilities', async () => {
|
||||
const { ref } = await vi.importActual<typeof import('vue')>('vue')
|
||||
mocks.usesMobilePresentation = ref(false)
|
||||
|
||||
return {
|
||||
useGlassMobilePresentation: () => mocks.usesMobilePresentation,
|
||||
}
|
||||
})
|
||||
|
||||
describe('GlassSettingsDialog', () => {
|
||||
beforeEach(() => {
|
||||
mocks.cancelGlassPreview.mockClear()
|
||||
mocks.commitGlassPreview.mockClear()
|
||||
mocks.previewGlassSettings.mockClear()
|
||||
mocks.usesMobilePresentation!.value = false
|
||||
mocks.display.smAndDown.value = false
|
||||
mocks.settings.value.glassAppearance = 'clear'
|
||||
mocks.settings.value.glassDeformationStrength = 50
|
||||
mocks.settings.value.glassFlowStrength = 50
|
||||
@@ -74,6 +89,7 @@ describe('GlassSettingsDialog', () => {
|
||||
})
|
||||
|
||||
it('cancels an active preview when the parent closes the dialog', async () => {
|
||||
mocks.display.smAndDown.value = true
|
||||
const wrapper = shallowMount(GlassSettingsDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
@@ -367,4 +383,40 @@ describe('GlassSettingsDialog', () => {
|
||||
'theme.glassFlowStrength',
|
||||
])
|
||||
})
|
||||
|
||||
it('restores motion tuning when a mobile presentation returns to desktop', async () => {
|
||||
mocks.usesMobilePresentation!.value = true
|
||||
mocks.settings.value.glassQuality = 'high'
|
||||
const wrapper = shallowMount(GlassSettingsDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
VCard: slotStub,
|
||||
VCardActions: slotStub,
|
||||
VCardText: slotStub,
|
||||
VDialog: dialogStub,
|
||||
VDialogCloseBtn: true,
|
||||
VSlider: sliderStub,
|
||||
},
|
||||
},
|
||||
props: { modelValue: true },
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('.slider-stub').map(slider => slider.attributes('aria-label'))).toEqual([
|
||||
'theme.glassTransparencyStrength',
|
||||
'theme.glassTransmissionStrength',
|
||||
'theme.glassReflectionStrength',
|
||||
])
|
||||
expect(wrapper.find('.glass-settings-dialog__live-controls').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('theme.glassMaterialStrengthHint')
|
||||
expect(wrapper.text()).not.toContain('theme.glassOpticalStrengthHint')
|
||||
expect(wrapper.text()).toContain('theme.glassQualityMobileHint')
|
||||
|
||||
mocks.usesMobilePresentation!.value = false
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll('.slider-stub')).toHaveLength(6)
|
||||
expect(wrapper.find('.glass-settings-dialog__live-controls').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('theme.glassMaterialStrengthHint')
|
||||
expect(wrapper.text()).toContain('theme.glassOpticalStrengthHint')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ThemeCustomizerGlassAppearance, ThemeCustomizerGlassQuality } from '@/composables/useThemeCustomizer'
|
||||
import { useGlassMobilePresentation } from '@/composables/useGlassPresentationCapabilities'
|
||||
import { usePagePresentationMotion } from '@/composables/usePagePresentationMotion'
|
||||
import {
|
||||
createGlassWallpaperSourceCache,
|
||||
@@ -72,7 +73,9 @@ const emit = defineEmits<{
|
||||
|
||||
const fixedCanvas = ref<HTMLCanvasElement | null>(null)
|
||||
const scrollCanvas = ref<HTMLCanvasElement | null>(null)
|
||||
const interactionSource = useGlassOpticalInteractionSource()
|
||||
const usesMobilePresentation = useGlassMobilePresentation()
|
||||
const dynamicsActive = computed(() => !usesMobilePresentation.value)
|
||||
const interactionSource = useGlassOpticalInteractionSource(dynamicsActive)
|
||||
const pagePresentationMotion = usePagePresentationMotion()
|
||||
const wallpaperSourceCache = createGlassWallpaperSourceCache()
|
||||
const fixedRenderer = useGlassOpticalRenderer({
|
||||
@@ -80,6 +83,7 @@ const fixedRenderer = useGlassOpticalRenderer({
|
||||
appearance: () => props.appearance,
|
||||
canvas: fixedCanvas,
|
||||
deformationStrength: () => props.deformationStrength,
|
||||
dynamicsActive,
|
||||
flowStrength: () => props.flowStrength,
|
||||
interactionSource,
|
||||
quality: () => props.quality,
|
||||
@@ -104,6 +108,7 @@ const scrollRenderer = useGlassOpticalRenderer({
|
||||
appearance: () => props.appearance,
|
||||
canvas: scrollCanvas,
|
||||
deformationStrength: () => props.deformationStrength,
|
||||
dynamicsActive,
|
||||
flowStrength: () => props.flowStrength,
|
||||
interactionSource,
|
||||
pageMotion: pagePresentationMotion.reader,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { ref } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import GlassOpticalLayer from '@/components/theme/GlassOpticalLayer.vue'
|
||||
|
||||
@@ -24,6 +24,9 @@ const rendererResults = vi.hoisted(
|
||||
}>,
|
||||
)
|
||||
const interactionSource = vi.hoisted(() => ({ subscribe: vi.fn() }))
|
||||
const mobilePresentationState = vi.hoisted(() => ({
|
||||
current: null as { value: boolean } | null,
|
||||
}))
|
||||
const wallpaperSourceCache = vi.hoisted(() => ({ get: vi.fn() }))
|
||||
const setRendererState = vi.hoisted(() =>
|
||||
vi.fn((state: { value: string }, value: string) => {
|
||||
@@ -100,7 +103,17 @@ vi.mock('@/composables/useGlassOpticalRenderer', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useGlassPresentationCapabilities', async () => {
|
||||
const { ref: createRef } = await vi.importActual<typeof import('vue')>('vue')
|
||||
mobilePresentationState.current = createRef(false)
|
||||
|
||||
return {
|
||||
useGlassMobilePresentation: () => mobilePresentationState.current,
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mobilePresentationState.current!.value = false
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
@@ -135,6 +148,7 @@ describe('GlassOpticalLayer', () => {
|
||||
expect(rendererCalls.every(options => options.interactionSource === interactionSource)).toBe(true)
|
||||
expect(rendererCalls.every(options => options.wallpaperSourceCache === wallpaperSourceCache)).toBe(true)
|
||||
expect(rendererCalls.every(options => options.syncDocumentState === false)).toBe(true)
|
||||
expect(rendererCalls.every(options => (options.dynamicsActive as { value: boolean }).value)).toBe(true)
|
||||
expect(rendererCalls[0].pageMotion).toBeUndefined()
|
||||
expect(rendererCalls[1].pageMotion).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -147,6 +161,39 @@ describe('GlassOpticalLayer', () => {
|
||||
expect(setRendererState).toHaveBeenLastCalledWith(expect.any(Object), 'fallback')
|
||||
})
|
||||
|
||||
it('keeps both material contexts while disabling dynamics on mobile presentations', async () => {
|
||||
rendererCalls.length = 0
|
||||
rendererResults.length = 0
|
||||
mobilePresentationState.current!.value = true
|
||||
const wrapper = shallowMount(GlassOpticalLayer, {
|
||||
props: {
|
||||
appearance: 'frosted',
|
||||
deformationStrength: 50,
|
||||
flowStrength: 50,
|
||||
previousWallpaperUrl: '',
|
||||
quality: 'high',
|
||||
reflectionStrength: 50,
|
||||
routeKey: '/dashboard',
|
||||
tintColor: '#8D51F9',
|
||||
transitionDuration: 1500,
|
||||
transitionStartedAt: 0,
|
||||
transmissionStrength: 50,
|
||||
translationStrength: 50,
|
||||
transparencyStrength: 50,
|
||||
wallpaperUrl: '/wallpaper.jpg',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('canvas')).toHaveLength(2)
|
||||
expect(rendererCalls.every(options => !(options.dynamicsActive as { value: boolean }).value)).toBe(true)
|
||||
|
||||
mobilePresentationState.current!.value = false
|
||||
await nextTick()
|
||||
|
||||
expect(rendererCalls.every(options => (options.dynamicsActive as { value: boolean }).value)).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('activates matching resource bundles in one shared animation frame', async () => {
|
||||
rendererCalls.length = 0
|
||||
rendererResults.length = 0
|
||||
|
||||
@@ -2408,6 +2408,87 @@ describe('glass optical surface discovery', () => {
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it('keeps high-quality material rendering while mobile dynamics are disabled', async () => {
|
||||
stubMediaPreferences({ coarsePointer: true })
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(390)
|
||||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(844)
|
||||
const three = await import('three')
|
||||
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||
const canvas = document.createElement('canvas')
|
||||
const deformationStrength = ref(80)
|
||||
const dynamicsActive = ref(false)
|
||||
const flowStrength = ref(80)
|
||||
const reflectionStrength = ref(80)
|
||||
const translationStrength = ref(80)
|
||||
const scope = effectScope()
|
||||
const renderer = scope.run(() =>
|
||||
useGlassOpticalRenderer({
|
||||
active: ref(true),
|
||||
appearance: ref('frosted'),
|
||||
canvas: ref(canvas),
|
||||
deformationStrength,
|
||||
dynamicsActive,
|
||||
flowStrength,
|
||||
quality: ref('high'),
|
||||
reflectionStrength,
|
||||
routeKey: ref('/dashboard'),
|
||||
tintColor: ref('#8D51F9'),
|
||||
transmissionStrength: ref(80),
|
||||
translationStrength,
|
||||
transparencyStrength: ref(80),
|
||||
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||
}),
|
||||
)
|
||||
|
||||
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||
const scene = render.mock.calls.at(-1)?.[0] as unknown as {
|
||||
children: Array<{
|
||||
material: {
|
||||
uniforms: {
|
||||
uDeformationStrength: { value: number }
|
||||
uFlowStrength: { value: number }
|
||||
uHasFlowTexture: { value: number }
|
||||
uReflectionStrength: { value: number }
|
||||
uTrailCount: { value: number }
|
||||
uTranslationStrength: { value: number }
|
||||
}
|
||||
}
|
||||
}>
|
||||
}
|
||||
const uniforms = scene.children[0].material.uniforms
|
||||
expect(uniforms.uTranslationStrength.value).toBe(0)
|
||||
expect(uniforms.uDeformationStrength.value).toBe(0)
|
||||
expect(uniforms.uFlowStrength.value).toBe(0)
|
||||
expect(uniforms.uTrailCount.value).toBe(0)
|
||||
expect(uniforms.uHasFlowTexture.value).toBe(0)
|
||||
expect(uniforms.uReflectionStrength.value).toBeGreaterThan(0)
|
||||
|
||||
const renderedFrames = renderer?.renderedFrames.value
|
||||
dispatchTouchEvent('touchstart', [{ clientX: 80, clientY: 180, identifier: 7 }])
|
||||
dispatchTouchEvent('touchmove', [{ clientX: 180, clientY: 320, identifier: 7 }])
|
||||
await nextTick()
|
||||
expect(renderer?.renderedFrames.value).toBe(renderedFrames)
|
||||
|
||||
translationStrength.value = 100
|
||||
deformationStrength.value = 100
|
||||
flowStrength.value = 100
|
||||
reflectionStrength.value = 100
|
||||
await nextTick()
|
||||
expect(uniforms.uTranslationStrength.value).toBe(0)
|
||||
expect(uniforms.uDeformationStrength.value).toBe(0)
|
||||
expect(uniforms.uFlowStrength.value).toBe(0)
|
||||
expect(uniforms.uReflectionStrength.value).toBeGreaterThan(1)
|
||||
|
||||
dynamicsActive.value = true
|
||||
await nextTick()
|
||||
expect(uniforms.uTranslationStrength.value).toBeGreaterThan(0)
|
||||
expect(uniforms.uDeformationStrength.value).toBeGreaterThan(0)
|
||||
expect(uniforms.uFlowStrength.value).toBeGreaterThan(0)
|
||||
expect(uniforms.uTrailCount.value).toBe(4)
|
||||
expect(uniforms.uHasFlowTexture.value).toBe(1)
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it('keeps scroll-space surface geometry stable while updating the visible viewport offset', async () => {
|
||||
stubMediaPreferences({ coarsePointer: true })
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(390)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useGlassMobilePresentation } from '@/composables/useGlassPresentationCapabilities'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
smAndDown: null as { value: boolean } | null,
|
||||
usesTouchInput: null as { value: boolean } | null,
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const { ref } = await vi.importActual<typeof import('vue')>('vue')
|
||||
mocks.usesTouchInput = ref(false)
|
||||
|
||||
return {
|
||||
useMediaQuery: vi.fn(() => mocks.usesTouchInput),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('vuetify', async () => {
|
||||
const { ref } = await vi.importActual<typeof import('vue')>('vue')
|
||||
mocks.smAndDown = ref(false)
|
||||
|
||||
return {
|
||||
useDisplay: () => ({ smAndDown: mocks.smAndDown }),
|
||||
}
|
||||
})
|
||||
|
||||
describe('useGlassMobilePresentation', () => {
|
||||
beforeEach(() => {
|
||||
mocks.smAndDown!.value = false
|
||||
mocks.usesTouchInput!.value = false
|
||||
})
|
||||
|
||||
it('reacts when DevTools switches between touch and desktop input', () => {
|
||||
const usesMobilePresentation = useGlassMobilePresentation()
|
||||
|
||||
mocks.usesTouchInput!.value = true
|
||||
expect(usesMobilePresentation.value).toBe(true)
|
||||
|
||||
mocks.usesTouchInput!.value = false
|
||||
expect(usesMobilePresentation.value).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps small viewports on the mobile presentation path', () => {
|
||||
const usesMobilePresentation = useGlassMobilePresentation()
|
||||
|
||||
mocks.smAndDown!.value = true
|
||||
expect(usesMobilePresentation.value).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -150,7 +150,9 @@ export function setGlassRendererState(state: Ref<GlassRendererState>, value: Gla
|
||||
}
|
||||
|
||||
/** 为有界的多个呈现 context 建立唯一的全局指针与触摸事件源。 */
|
||||
export function useGlassOpticalInteractionSource(): GlassOpticalInteractionSource {
|
||||
export function useGlassOpticalInteractionSource(
|
||||
active: MaybeRefOrGetter<boolean> = true,
|
||||
): GlassOpticalInteractionSource {
|
||||
const listeners: Record<GlassPresentationSpace, Set<(event: PointerEvent | TouchEvent) => void>> = {
|
||||
fixed: new Set(),
|
||||
scroll: new Set(),
|
||||
@@ -197,6 +199,13 @@ export function useGlassOpticalInteractionSource(): GlassOpticalInteractionSourc
|
||||
event.type.startsWith('touch')
|
||||
|
||||
const dispatch = (event: PointerEvent | TouchEvent) => {
|
||||
if (!toValue(active)) {
|
||||
if (isTouchInteractionEvent(event) && (event.type === 'touchend' || event.type === 'touchcancel')) {
|
||||
for (const touch of Array.from(event.changedTouches)) touchOwners.delete(touch.identifier)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const owner = isTouchInteractionEvent(event)
|
||||
? resolveTouchOwner(event)
|
||||
: resolvePointOwner(event.clientX, event.clientY)
|
||||
@@ -327,6 +336,8 @@ interface UseGlassOpticalRendererOptions {
|
||||
appearance: MaybeRefOrGetter<ThemeCustomizerGlassAppearance>
|
||||
canvas: Ref<HTMLCanvasElement | null>
|
||||
deformationStrength?: MaybeRefOrGetter<number>
|
||||
/** 是否启用交互形变、尾迹和 temporal flow;静态材质不受影响。 */
|
||||
dynamicsActive?: MaybeRefOrGetter<boolean>
|
||||
flowStrength?: MaybeRefOrGetter<number>
|
||||
interactionSource?: GlassOpticalInteractionSource
|
||||
/** 旧调用方的单一动态强度仅作为三个独立维度的兼容回退。 */
|
||||
@@ -1768,7 +1779,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
if (!resources || !three) return
|
||||
|
||||
const profile = getRenderProfile()
|
||||
if (!profile.flowField) {
|
||||
if (!hasDynamicCapability() || !profile.flowField) {
|
||||
disposeFlowResources()
|
||||
return
|
||||
}
|
||||
@@ -2343,23 +2354,37 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
return toValue(options.motionStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT)
|
||||
}
|
||||
|
||||
function hasDynamicCapability() {
|
||||
return toValue(options.dynamicsActive ?? true)
|
||||
}
|
||||
|
||||
function getTranslationStrengthScale() {
|
||||
if (!hasDynamicCapability()) return 0
|
||||
|
||||
return getGlassOpticalTranslationStrengthScale(toValue(options.translationStrength ?? getLegacyDynamicStrength()))
|
||||
}
|
||||
|
||||
function getDeformationStrengthScale() {
|
||||
if (!hasDynamicCapability()) return 0
|
||||
|
||||
return getGlassOpticalDeformationStrengthScale(toValue(options.deformationStrength ?? getLegacyDynamicStrength()))
|
||||
}
|
||||
|
||||
function getFlowStrengthScale() {
|
||||
if (!hasDynamicCapability()) return 0
|
||||
|
||||
return getGlassOpticalFlowStrengthScale(toValue(options.flowStrength ?? getLegacyDynamicStrength()))
|
||||
}
|
||||
|
||||
function getMotionExpansion() {
|
||||
if (!hasDynamicCapability()) return 0
|
||||
|
||||
return getGlassOpticalMotionExpansion(toValue(options.flowStrength ?? getLegacyDynamicStrength()))
|
||||
}
|
||||
|
||||
function getMaxRefractionPixels() {
|
||||
if (!hasDynamicCapability()) return 0
|
||||
|
||||
return getGlassOpticalMaxRefractionPixels(
|
||||
getRenderProfile().maxRefractionPixels,
|
||||
toValue(options.deformationStrength ?? getLegacyDynamicStrength()),
|
||||
@@ -2639,6 +2664,8 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
timestamp: number,
|
||||
velocityOverride?: { x: number; y: number },
|
||||
) {
|
||||
if (!hasDynamicCapability()) return
|
||||
|
||||
const viewportWidth = Math.max(window.innerWidth, 1)
|
||||
const viewportHeight = Math.max(window.innerHeight, 1)
|
||||
const presentation = getCommittedPresentationSize()
|
||||
@@ -2779,6 +2806,8 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
}
|
||||
|
||||
function handleInteractionEvent(event: PointerEvent | TouchEvent) {
|
||||
if (!hasDynamicCapability()) return
|
||||
|
||||
if (event.type === 'pointermove') {
|
||||
handlePointerMove(event as PointerEvent)
|
||||
return
|
||||
@@ -3707,7 +3736,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
},
|
||||
uTranslationStrength: { value: getTranslationStrengthScale() },
|
||||
uTrail: { value: Array.from({ length: 4 }, () => new Vector4Class(0.5, 0.5, 0, 0)) },
|
||||
uTrailCount: { value: getRenderProfile().trailCount },
|
||||
uTrailCount: { value: hasDynamicCapability() ? getRenderProfile().trailCount : 0 },
|
||||
uVisibleViewportSize: { value: new three.Vector2(window.innerWidth, window.innerHeight) },
|
||||
uScrollOffset: { value: new three.Vector2(0, 0) },
|
||||
uWakeDirection: { value: new three.Vector2(0, -1) },
|
||||
@@ -3847,6 +3876,31 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => toValue(options.dynamicsActive ?? true),
|
||||
active => {
|
||||
if (!resources) return
|
||||
|
||||
interactionAnimating = false
|
||||
activeTouchIdentifier = null
|
||||
cancelScheduledFrame()
|
||||
resetInteractionState()
|
||||
resources.uniforms.uTranslationStrength.value = active ? getTranslationStrengthScale() : 0
|
||||
resources.uniforms.uDeformationStrength.value = active ? getDeformationStrengthScale() : 0
|
||||
resources.uniforms.uFlowStrength.value = active ? getFlowStrengthScale() : 0
|
||||
resources.uniforms.uMotionExpansion.value = active ? getMotionExpansion() : 0
|
||||
resources.uniforms.uMaxRefractionPixels.value = active
|
||||
? getGlassOpticalMaxRefractionPixels(
|
||||
getRenderProfile().maxRefractionPixels,
|
||||
toValue(options.deformationStrength ?? getLegacyDynamicStrength()),
|
||||
)
|
||||
: 0
|
||||
resources.uniforms.uTrailCount.value = active ? getRenderProfile().trailCount : 0
|
||||
syncFlowResources()
|
||||
scheduleFrame()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => toValue(options.quality),
|
||||
async (quality, previousQuality) => {
|
||||
@@ -3857,12 +3911,14 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
const applyQuality = () => {
|
||||
if (!resources || toValue(options.quality) !== quality) return
|
||||
|
||||
resources.uniforms.uMaxRefractionPixels.value = getGlassOpticalMaxRefractionPixels(
|
||||
nextProfile.maxRefractionPixels,
|
||||
toValue(options.deformationStrength ?? getLegacyDynamicStrength()),
|
||||
)
|
||||
resources.uniforms.uMaxRefractionPixels.value = hasDynamicCapability()
|
||||
? getGlassOpticalMaxRefractionPixels(
|
||||
nextProfile.maxRefractionPixels,
|
||||
toValue(options.deformationStrength ?? getLegacyDynamicStrength()),
|
||||
)
|
||||
: 0
|
||||
resources.uniforms.uQuality.value = quality === 'high' ? 1 : 0
|
||||
resources.uniforms.uTrailCount.value = nextProfile.trailCount
|
||||
resources.uniforms.uTrailCount.value = hasDynamicCapability() ? nextProfile.trailCount : 0
|
||||
interactionAnimating = false
|
||||
cancelScheduledFrame()
|
||||
resetInteractionState()
|
||||
@@ -3912,14 +3968,18 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
||||
]) => {
|
||||
if (!resources) return
|
||||
|
||||
resources.uniforms.uTranslationStrength.value = getGlassOpticalTranslationStrengthScale(translationStrength)
|
||||
resources.uniforms.uDeformationStrength.value = getGlassOpticalDeformationStrengthScale(deformationStrength)
|
||||
resources.uniforms.uFlowStrength.value = getGlassOpticalFlowStrengthScale(flowStrength)
|
||||
resources.uniforms.uMotionExpansion.value = getGlassOpticalMotionExpansion(flowStrength)
|
||||
resources.uniforms.uMaxRefractionPixels.value = getGlassOpticalMaxRefractionPixels(
|
||||
getRenderProfile().maxRefractionPixels,
|
||||
deformationStrength,
|
||||
)
|
||||
const dynamicsActive = hasDynamicCapability()
|
||||
resources.uniforms.uTranslationStrength.value = dynamicsActive
|
||||
? getGlassOpticalTranslationStrengthScale(translationStrength)
|
||||
: 0
|
||||
resources.uniforms.uDeformationStrength.value = dynamicsActive
|
||||
? getGlassOpticalDeformationStrengthScale(deformationStrength)
|
||||
: 0
|
||||
resources.uniforms.uFlowStrength.value = dynamicsActive ? getGlassOpticalFlowStrengthScale(flowStrength) : 0
|
||||
resources.uniforms.uMotionExpansion.value = dynamicsActive ? getGlassOpticalMotionExpansion(flowStrength) : 0
|
||||
resources.uniforms.uMaxRefractionPixels.value = dynamicsActive
|
||||
? getGlassOpticalMaxRefractionPixels(getRenderProfile().maxRefractionPixels, deformationStrength)
|
||||
: 0
|
||||
resources.uniforms.uReflectionStrength.value = getGlassOpticalReflectionStrengthScale(reflectionStrength)
|
||||
const materialResponse = getGlassMaterialResponse(toValue(options.appearance), transparencyStrength)
|
||||
resources.uniforms.uBackgroundVisibility.value = materialResponse.backgroundVisibility
|
||||
|
||||
13
src/composables/useGlassPresentationCapabilities.ts
Normal file
13
src/composables/useGlassPresentationCapabilities.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const TOUCH_PRESENTATION_QUERY = '(hover: none), (pointer: coarse)'
|
||||
|
||||
/** 根据当前视口和主输入能力判断玻璃是否使用移动端静态呈现。 */
|
||||
export function useGlassMobilePresentation() {
|
||||
const { smAndDown } = useDisplay()
|
||||
const usesTouchInput = useMediaQuery(TOUCH_PRESENTATION_QUERY)
|
||||
|
||||
return computed(() => smAndDown.value || usesTouchInput.value)
|
||||
}
|
||||
@@ -160,6 +160,7 @@ export default {
|
||||
'Static CSS material with the lowest resource use; clarity, transmission, and reflection remain available.',
|
||||
glassQualityBalancedHint: 'Shared live refraction that balances liquid feedback and GPU use.',
|
||||
glassQualityHighHint: 'Full temporal flow, diffusion detail, and content protection at a higher GPU cost.',
|
||||
glassQualityMobileHint: 'Keeps the selected material quality; real-time motion is disabled on mobile.',
|
||||
glassPreset: 'Preset',
|
||||
glassPresetNatural: 'Natural',
|
||||
glassPresetGlide: 'Glide',
|
||||
@@ -172,10 +173,10 @@ export default {
|
||||
glassReflectionStrength: 'Reflection Brightness',
|
||||
glassTransmissionStrength: 'Transmission Brightness',
|
||||
glassTransparencyStrength: 'Clarity',
|
||||
glassMaterialStrengthHint:
|
||||
'Clarity controls wallpaper texture visibility. Transmission Brightness controls the background inside the glass. Reflection Brightness controls surface reflections and highlights.',
|
||||
glassOpticalStrengthHint:
|
||||
'Sample Translation moves the shared wallpaper. Deformation controls local bending. Flow Strength controls trail range and inertia.',
|
||||
glassOpticalStrengthUnavailableHint:
|
||||
'Standard quality keeps all three material controls; switch to Balanced or High to adjust sample translation, deformation, and flow.',
|
||||
purple: 'Purple',
|
||||
custom: 'Custom Style',
|
||||
transparency: 'Transparency',
|
||||
|
||||
@@ -157,6 +157,7 @@ export default {
|
||||
glassQualityCssHint: '静态 CSS 材质,资源占用最低,保留通透、透射与反射,不启用实时流动。',
|
||||
glassQualityBalancedHint: '共享实时折射,优先平衡流动反馈与 GPU 占用。',
|
||||
glassQualityHighHint: '完整时序流场、扩散细节与内容保护,GPU 占用更高。',
|
||||
glassQualityMobileHint: '保留所选档位的材质精度;移动端不启用实时流动。',
|
||||
glassPreset: '方案',
|
||||
glassPresetNatural: '自然',
|
||||
glassPresetGlide: '滑移',
|
||||
@@ -169,8 +170,9 @@ export default {
|
||||
glassReflectionStrength: '反射亮度',
|
||||
glassTransmissionStrength: '透射亮度',
|
||||
glassTransparencyStrength: '通透度',
|
||||
glassMaterialStrengthHint:
|
||||
'通透度控制壁纸纹理的可见程度,透射亮度控制玻璃内的背景明暗,反射亮度控制表面反射与高光强度。',
|
||||
glassOpticalStrengthHint: '采样平移控制整体滑移,形变强度控制局部弯曲,流动强度控制轨迹范围与惯性。',
|
||||
glassOpticalStrengthUnavailableHint: '标准质量保留三项材质参数,切换到均衡或高质量后,可调整采样平移、形变和流动。',
|
||||
purple: '幻紫',
|
||||
custom: '附加样式',
|
||||
transparency: '透明度',
|
||||
|
||||
@@ -157,6 +157,7 @@ export default {
|
||||
glassQualityCssHint: '靜態 CSS 材質,資源佔用最低,保留通透、透射與反射,不啟用即時流動。',
|
||||
glassQualityBalancedHint: '共享即時折射,優先平衡流動回饋與 GPU 佔用。',
|
||||
glassQualityHighHint: '完整時序流場、擴散細節與內容保護,GPU 佔用更高。',
|
||||
glassQualityMobileHint: '保留所選檔位的材質精度;行動裝置不啟用即時流動。',
|
||||
glassPreset: '方案',
|
||||
glassPresetNatural: '自然',
|
||||
glassPresetGlide: '滑移',
|
||||
@@ -169,8 +170,9 @@ export default {
|
||||
glassReflectionStrength: '反射亮度',
|
||||
glassTransmissionStrength: '透射亮度',
|
||||
glassTransparencyStrength: '通透度',
|
||||
glassMaterialStrengthHint:
|
||||
'通透度控制桌布紋理的可見程度,透射亮度控制玻璃內的背景明暗,反射亮度控制表面反射與高光強度。',
|
||||
glassOpticalStrengthHint: '採樣平移控制整體滑移,形變強度控制局部彎曲,流動強度控制軌跡範圍與慣性。',
|
||||
glassOpticalStrengthUnavailableHint: '標準品質保留三項材質參數,切換到均衡或高品質後,可調整採樣平移、形變和流動。',
|
||||
purple: '幻紫',
|
||||
custom: '附加樣式',
|
||||
transparency: '透明度',
|
||||
|
||||
Reference in New Issue
Block a user