mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-08 17:26:41 +08:00
fix(glass): disable mobile motion dynamics (#623)
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
type GlassOpticalPreset,
|
type GlassOpticalPreset,
|
||||||
type GlassOpticalPresetOverrides,
|
type GlassOpticalPresetOverrides,
|
||||||
} from '@/utils/glassOptics'
|
} from '@/utils/glassOptics'
|
||||||
|
import { useGlassMobilePresentation } from '@/composables/useGlassPresentationCapabilities'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
|
const usesMobilePresentation = useGlassMobilePresentation()
|
||||||
const { settings } = useThemeCustomizer()
|
const { settings } = useThemeCustomizer()
|
||||||
const draftAppearance = ref<ThemeCustomizerGlassAppearance>(settings.value.glassAppearance)
|
const draftAppearance = ref<ThemeCustomizerGlassAppearance>(settings.value.glassAppearance)
|
||||||
const draftDeformationStrength = ref(settings.value.glassDeformationStrength)
|
const draftDeformationStrength = ref(settings.value.glassDeformationStrength)
|
||||||
@@ -51,7 +53,7 @@ const draftTranslationStrength = ref(settings.value.glassTranslationStrength)
|
|||||||
const draftTransparencyStrength = ref(settings.value.glassTransparencyStrength)
|
const draftTransparencyStrength = ref(settings.value.glassTransparencyStrength)
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
const usesRealtimeOptics = computed(() => draftQuality.value !== 'css')
|
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 availablePresets = computed(() => getAvailableGlassOpticalPresets(draftQuality.value))
|
||||||
const activePreset = computed<GlassOpticalPreset>(() =>
|
const activePreset = computed<GlassOpticalPreset>(() =>
|
||||||
availablePresets.value.includes(draftPreset.value) ? draftPreset.value : 'natural',
|
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.glassQualityBalancedHint', label: 'theme.glassQualityBalanced', value: 'balanced' },
|
||||||
{ hint: 'theme.glassQualityHighHint', label: 'theme.glassQualityHigh', value: 'high' },
|
{ 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 }> = [
|
const presetOptions: Array<{ label: string; value: GlassOpticalPreset }> = [
|
||||||
{ label: 'theme.glassPresetNatural', value: 'natural' },
|
{ label: 'theme.glassPresetNatural', value: 'natural' },
|
||||||
{ label: 'theme.glassPresetGlide', value: 'glide' },
|
{ label: 'theme.glassPresetGlide', value: 'glide' },
|
||||||
@@ -410,6 +416,9 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
thumb-label
|
thumb-label
|
||||||
@update:model-value="updateReflectionStrength"
|
@update:model-value="updateReflectionStrength"
|
||||||
/>
|
/>
|
||||||
|
<p class="glass-settings-dialog__hint">
|
||||||
|
{{ t('theme.glassMaterialStrengthHint') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div v-if="showsDynamicTuning" class="glass-settings-dialog__live-controls">
|
<div v-if="showsDynamicTuning" class="glass-settings-dialog__live-controls">
|
||||||
<h3 class="glass-settings-dialog__group-label">{{ t('theme.glassDynamicTuning') }}</h3>
|
<h3 class="glass-settings-dialog__group-label">{{ t('theme.glassDynamicTuning') }}</h3>
|
||||||
@@ -463,10 +472,10 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
thumb-label
|
thumb-label
|
||||||
@update:model-value="updateFlowStrength"
|
@update:model-value="updateFlowStrength"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<p class="glass-settings-dialog__hint">
|
<p class="glass-settings-dialog__hint">
|
||||||
{{ t(showsDynamicTuning ? 'theme.glassOpticalStrengthHint' : 'theme.glassOpticalStrengthUnavailableHint') }}
|
{{ t('theme.glassOpticalStrengthHint') }}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ const mocks = vi.hoisted(() => ({
|
|||||||
cancelGlassPreview: vi.fn(),
|
cancelGlassPreview: vi.fn(),
|
||||||
commitGlassPreview: vi.fn(),
|
commitGlassPreview: vi.fn(),
|
||||||
previewGlassSettings: vi.fn(),
|
previewGlassSettings: vi.fn(),
|
||||||
|
usesMobilePresentation: null as { value: boolean } | null,
|
||||||
|
display: {
|
||||||
|
smAndDown: { value: false },
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
value: {
|
value: {
|
||||||
glassAppearance: 'clear',
|
glassAppearance: 'clear',
|
||||||
@@ -53,14 +57,25 @@ vi.mock('vue-i18n', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('vuetify', () => ({
|
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', () => {
|
describe('GlassSettingsDialog', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.cancelGlassPreview.mockClear()
|
mocks.cancelGlassPreview.mockClear()
|
||||||
mocks.commitGlassPreview.mockClear()
|
mocks.commitGlassPreview.mockClear()
|
||||||
mocks.previewGlassSettings.mockClear()
|
mocks.previewGlassSettings.mockClear()
|
||||||
|
mocks.usesMobilePresentation!.value = false
|
||||||
|
mocks.display.smAndDown.value = false
|
||||||
mocks.settings.value.glassAppearance = 'clear'
|
mocks.settings.value.glassAppearance = 'clear'
|
||||||
mocks.settings.value.glassDeformationStrength = 50
|
mocks.settings.value.glassDeformationStrength = 50
|
||||||
mocks.settings.value.glassFlowStrength = 50
|
mocks.settings.value.glassFlowStrength = 50
|
||||||
@@ -74,6 +89,7 @@ describe('GlassSettingsDialog', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('cancels an active preview when the parent closes the dialog', async () => {
|
it('cancels an active preview when the parent closes the dialog', async () => {
|
||||||
|
mocks.display.smAndDown.value = true
|
||||||
const wrapper = shallowMount(GlassSettingsDialog, {
|
const wrapper = shallowMount(GlassSettingsDialog, {
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
@@ -367,4 +383,40 @@ describe('GlassSettingsDialog', () => {
|
|||||||
'theme.glassFlowStrength',
|
'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">
|
<script setup lang="ts">
|
||||||
import type { ThemeCustomizerGlassAppearance, ThemeCustomizerGlassQuality } from '@/composables/useThemeCustomizer'
|
import type { ThemeCustomizerGlassAppearance, ThemeCustomizerGlassQuality } from '@/composables/useThemeCustomizer'
|
||||||
|
import { useGlassMobilePresentation } from '@/composables/useGlassPresentationCapabilities'
|
||||||
import { usePagePresentationMotion } from '@/composables/usePagePresentationMotion'
|
import { usePagePresentationMotion } from '@/composables/usePagePresentationMotion'
|
||||||
import {
|
import {
|
||||||
createGlassWallpaperSourceCache,
|
createGlassWallpaperSourceCache,
|
||||||
@@ -72,7 +73,9 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const fixedCanvas = ref<HTMLCanvasElement | null>(null)
|
const fixedCanvas = ref<HTMLCanvasElement | null>(null)
|
||||||
const scrollCanvas = 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 pagePresentationMotion = usePagePresentationMotion()
|
||||||
const wallpaperSourceCache = createGlassWallpaperSourceCache()
|
const wallpaperSourceCache = createGlassWallpaperSourceCache()
|
||||||
const fixedRenderer = useGlassOpticalRenderer({
|
const fixedRenderer = useGlassOpticalRenderer({
|
||||||
@@ -80,6 +83,7 @@ const fixedRenderer = useGlassOpticalRenderer({
|
|||||||
appearance: () => props.appearance,
|
appearance: () => props.appearance,
|
||||||
canvas: fixedCanvas,
|
canvas: fixedCanvas,
|
||||||
deformationStrength: () => props.deformationStrength,
|
deformationStrength: () => props.deformationStrength,
|
||||||
|
dynamicsActive,
|
||||||
flowStrength: () => props.flowStrength,
|
flowStrength: () => props.flowStrength,
|
||||||
interactionSource,
|
interactionSource,
|
||||||
quality: () => props.quality,
|
quality: () => props.quality,
|
||||||
@@ -104,6 +108,7 @@ const scrollRenderer = useGlassOpticalRenderer({
|
|||||||
appearance: () => props.appearance,
|
appearance: () => props.appearance,
|
||||||
canvas: scrollCanvas,
|
canvas: scrollCanvas,
|
||||||
deformationStrength: () => props.deformationStrength,
|
deformationStrength: () => props.deformationStrength,
|
||||||
|
dynamicsActive,
|
||||||
flowStrength: () => props.flowStrength,
|
flowStrength: () => props.flowStrength,
|
||||||
interactionSource,
|
interactionSource,
|
||||||
pageMotion: pagePresentationMotion.reader,
|
pageMotion: pagePresentationMotion.reader,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { shallowMount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import { ref } from 'vue'
|
import { nextTick, ref } from 'vue'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import GlassOpticalLayer from '@/components/theme/GlassOpticalLayer.vue'
|
import GlassOpticalLayer from '@/components/theme/GlassOpticalLayer.vue'
|
||||||
|
|
||||||
@@ -24,6 +24,9 @@ const rendererResults = vi.hoisted(
|
|||||||
}>,
|
}>,
|
||||||
)
|
)
|
||||||
const interactionSource = vi.hoisted(() => ({ subscribe: vi.fn() }))
|
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 wallpaperSourceCache = vi.hoisted(() => ({ get: vi.fn() }))
|
||||||
const setRendererState = vi.hoisted(() =>
|
const setRendererState = vi.hoisted(() =>
|
||||||
vi.fn((state: { value: string }, value: string) => {
|
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(() => {
|
afterEach(() => {
|
||||||
|
mobilePresentationState.current!.value = false
|
||||||
vi.unstubAllGlobals()
|
vi.unstubAllGlobals()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -135,6 +148,7 @@ describe('GlassOpticalLayer', () => {
|
|||||||
expect(rendererCalls.every(options => options.interactionSource === interactionSource)).toBe(true)
|
expect(rendererCalls.every(options => options.interactionSource === interactionSource)).toBe(true)
|
||||||
expect(rendererCalls.every(options => options.wallpaperSourceCache === wallpaperSourceCache)).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.syncDocumentState === false)).toBe(true)
|
||||||
|
expect(rendererCalls.every(options => (options.dynamicsActive as { value: boolean }).value)).toBe(true)
|
||||||
expect(rendererCalls[0].pageMotion).toBeUndefined()
|
expect(rendererCalls[0].pageMotion).toBeUndefined()
|
||||||
expect(rendererCalls[1].pageMotion).toEqual(
|
expect(rendererCalls[1].pageMotion).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -147,6 +161,39 @@ describe('GlassOpticalLayer', () => {
|
|||||||
expect(setRendererState).toHaveBeenLastCalledWith(expect.any(Object), 'fallback')
|
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 () => {
|
it('activates matching resource bundles in one shared animation frame', async () => {
|
||||||
rendererCalls.length = 0
|
rendererCalls.length = 0
|
||||||
rendererResults.length = 0
|
rendererResults.length = 0
|
||||||
|
|||||||
@@ -2408,6 +2408,87 @@ describe('glass optical surface discovery', () => {
|
|||||||
scope.stop()
|
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 () => {
|
it('keeps scroll-space surface geometry stable while updating the visible viewport offset', async () => {
|
||||||
stubMediaPreferences({ coarsePointer: true })
|
stubMediaPreferences({ coarsePointer: true })
|
||||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(390)
|
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 建立唯一的全局指针与触摸事件源。 */
|
/** 为有界的多个呈现 context 建立唯一的全局指针与触摸事件源。 */
|
||||||
export function useGlassOpticalInteractionSource(): GlassOpticalInteractionSource {
|
export function useGlassOpticalInteractionSource(
|
||||||
|
active: MaybeRefOrGetter<boolean> = true,
|
||||||
|
): GlassOpticalInteractionSource {
|
||||||
const listeners: Record<GlassPresentationSpace, Set<(event: PointerEvent | TouchEvent) => void>> = {
|
const listeners: Record<GlassPresentationSpace, Set<(event: PointerEvent | TouchEvent) => void>> = {
|
||||||
fixed: new Set(),
|
fixed: new Set(),
|
||||||
scroll: new Set(),
|
scroll: new Set(),
|
||||||
@@ -197,6 +199,13 @@ export function useGlassOpticalInteractionSource(): GlassOpticalInteractionSourc
|
|||||||
event.type.startsWith('touch')
|
event.type.startsWith('touch')
|
||||||
|
|
||||||
const dispatch = (event: PointerEvent | TouchEvent) => {
|
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)
|
const owner = isTouchInteractionEvent(event)
|
||||||
? resolveTouchOwner(event)
|
? resolveTouchOwner(event)
|
||||||
: resolvePointOwner(event.clientX, event.clientY)
|
: resolvePointOwner(event.clientX, event.clientY)
|
||||||
@@ -327,6 +336,8 @@ interface UseGlassOpticalRendererOptions {
|
|||||||
appearance: MaybeRefOrGetter<ThemeCustomizerGlassAppearance>
|
appearance: MaybeRefOrGetter<ThemeCustomizerGlassAppearance>
|
||||||
canvas: Ref<HTMLCanvasElement | null>
|
canvas: Ref<HTMLCanvasElement | null>
|
||||||
deformationStrength?: MaybeRefOrGetter<number>
|
deformationStrength?: MaybeRefOrGetter<number>
|
||||||
|
/** 是否启用交互形变、尾迹和 temporal flow;静态材质不受影响。 */
|
||||||
|
dynamicsActive?: MaybeRefOrGetter<boolean>
|
||||||
flowStrength?: MaybeRefOrGetter<number>
|
flowStrength?: MaybeRefOrGetter<number>
|
||||||
interactionSource?: GlassOpticalInteractionSource
|
interactionSource?: GlassOpticalInteractionSource
|
||||||
/** 旧调用方的单一动态强度仅作为三个独立维度的兼容回退。 */
|
/** 旧调用方的单一动态强度仅作为三个独立维度的兼容回退。 */
|
||||||
@@ -1768,7 +1779,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
if (!resources || !three) return
|
if (!resources || !three) return
|
||||||
|
|
||||||
const profile = getRenderProfile()
|
const profile = getRenderProfile()
|
||||||
if (!profile.flowField) {
|
if (!hasDynamicCapability() || !profile.flowField) {
|
||||||
disposeFlowResources()
|
disposeFlowResources()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2343,23 +2354,37 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
return toValue(options.motionStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT)
|
return toValue(options.motionStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasDynamicCapability() {
|
||||||
|
return toValue(options.dynamicsActive ?? true)
|
||||||
|
}
|
||||||
|
|
||||||
function getTranslationStrengthScale() {
|
function getTranslationStrengthScale() {
|
||||||
|
if (!hasDynamicCapability()) return 0
|
||||||
|
|
||||||
return getGlassOpticalTranslationStrengthScale(toValue(options.translationStrength ?? getLegacyDynamicStrength()))
|
return getGlassOpticalTranslationStrengthScale(toValue(options.translationStrength ?? getLegacyDynamicStrength()))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeformationStrengthScale() {
|
function getDeformationStrengthScale() {
|
||||||
|
if (!hasDynamicCapability()) return 0
|
||||||
|
|
||||||
return getGlassOpticalDeformationStrengthScale(toValue(options.deformationStrength ?? getLegacyDynamicStrength()))
|
return getGlassOpticalDeformationStrengthScale(toValue(options.deformationStrength ?? getLegacyDynamicStrength()))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFlowStrengthScale() {
|
function getFlowStrengthScale() {
|
||||||
|
if (!hasDynamicCapability()) return 0
|
||||||
|
|
||||||
return getGlassOpticalFlowStrengthScale(toValue(options.flowStrength ?? getLegacyDynamicStrength()))
|
return getGlassOpticalFlowStrengthScale(toValue(options.flowStrength ?? getLegacyDynamicStrength()))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMotionExpansion() {
|
function getMotionExpansion() {
|
||||||
|
if (!hasDynamicCapability()) return 0
|
||||||
|
|
||||||
return getGlassOpticalMotionExpansion(toValue(options.flowStrength ?? getLegacyDynamicStrength()))
|
return getGlassOpticalMotionExpansion(toValue(options.flowStrength ?? getLegacyDynamicStrength()))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMaxRefractionPixels() {
|
function getMaxRefractionPixels() {
|
||||||
|
if (!hasDynamicCapability()) return 0
|
||||||
|
|
||||||
return getGlassOpticalMaxRefractionPixels(
|
return getGlassOpticalMaxRefractionPixels(
|
||||||
getRenderProfile().maxRefractionPixels,
|
getRenderProfile().maxRefractionPixels,
|
||||||
toValue(options.deformationStrength ?? getLegacyDynamicStrength()),
|
toValue(options.deformationStrength ?? getLegacyDynamicStrength()),
|
||||||
@@ -2639,6 +2664,8 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
timestamp: number,
|
timestamp: number,
|
||||||
velocityOverride?: { x: number; y: number },
|
velocityOverride?: { x: number; y: number },
|
||||||
) {
|
) {
|
||||||
|
if (!hasDynamicCapability()) return
|
||||||
|
|
||||||
const viewportWidth = Math.max(window.innerWidth, 1)
|
const viewportWidth = Math.max(window.innerWidth, 1)
|
||||||
const viewportHeight = Math.max(window.innerHeight, 1)
|
const viewportHeight = Math.max(window.innerHeight, 1)
|
||||||
const presentation = getCommittedPresentationSize()
|
const presentation = getCommittedPresentationSize()
|
||||||
@@ -2779,6 +2806,8 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleInteractionEvent(event: PointerEvent | TouchEvent) {
|
function handleInteractionEvent(event: PointerEvent | TouchEvent) {
|
||||||
|
if (!hasDynamicCapability()) return
|
||||||
|
|
||||||
if (event.type === 'pointermove') {
|
if (event.type === 'pointermove') {
|
||||||
handlePointerMove(event as PointerEvent)
|
handlePointerMove(event as PointerEvent)
|
||||||
return
|
return
|
||||||
@@ -3707,7 +3736,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
},
|
},
|
||||||
uTranslationStrength: { value: getTranslationStrengthScale() },
|
uTranslationStrength: { value: getTranslationStrengthScale() },
|
||||||
uTrail: { value: Array.from({ length: 4 }, () => new Vector4Class(0.5, 0.5, 0, 0)) },
|
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) },
|
uVisibleViewportSize: { value: new three.Vector2(window.innerWidth, window.innerHeight) },
|
||||||
uScrollOffset: { value: new three.Vector2(0, 0) },
|
uScrollOffset: { value: new three.Vector2(0, 0) },
|
||||||
uWakeDirection: { value: new three.Vector2(0, -1) },
|
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(
|
watch(
|
||||||
() => toValue(options.quality),
|
() => toValue(options.quality),
|
||||||
async (quality, previousQuality) => {
|
async (quality, previousQuality) => {
|
||||||
@@ -3857,12 +3911,14 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
const applyQuality = () => {
|
const applyQuality = () => {
|
||||||
if (!resources || toValue(options.quality) !== quality) return
|
if (!resources || toValue(options.quality) !== quality) return
|
||||||
|
|
||||||
resources.uniforms.uMaxRefractionPixels.value = getGlassOpticalMaxRefractionPixels(
|
resources.uniforms.uMaxRefractionPixels.value = hasDynamicCapability()
|
||||||
|
? getGlassOpticalMaxRefractionPixels(
|
||||||
nextProfile.maxRefractionPixels,
|
nextProfile.maxRefractionPixels,
|
||||||
toValue(options.deformationStrength ?? getLegacyDynamicStrength()),
|
toValue(options.deformationStrength ?? getLegacyDynamicStrength()),
|
||||||
)
|
)
|
||||||
|
: 0
|
||||||
resources.uniforms.uQuality.value = quality === 'high' ? 1 : 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
|
interactionAnimating = false
|
||||||
cancelScheduledFrame()
|
cancelScheduledFrame()
|
||||||
resetInteractionState()
|
resetInteractionState()
|
||||||
@@ -3912,14 +3968,18 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
]) => {
|
]) => {
|
||||||
if (!resources) return
|
if (!resources) return
|
||||||
|
|
||||||
resources.uniforms.uTranslationStrength.value = getGlassOpticalTranslationStrengthScale(translationStrength)
|
const dynamicsActive = hasDynamicCapability()
|
||||||
resources.uniforms.uDeformationStrength.value = getGlassOpticalDeformationStrengthScale(deformationStrength)
|
resources.uniforms.uTranslationStrength.value = dynamicsActive
|
||||||
resources.uniforms.uFlowStrength.value = getGlassOpticalFlowStrengthScale(flowStrength)
|
? getGlassOpticalTranslationStrengthScale(translationStrength)
|
||||||
resources.uniforms.uMotionExpansion.value = getGlassOpticalMotionExpansion(flowStrength)
|
: 0
|
||||||
resources.uniforms.uMaxRefractionPixels.value = getGlassOpticalMaxRefractionPixels(
|
resources.uniforms.uDeformationStrength.value = dynamicsActive
|
||||||
getRenderProfile().maxRefractionPixels,
|
? getGlassOpticalDeformationStrengthScale(deformationStrength)
|
||||||
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)
|
resources.uniforms.uReflectionStrength.value = getGlassOpticalReflectionStrengthScale(reflectionStrength)
|
||||||
const materialResponse = getGlassMaterialResponse(toValue(options.appearance), transparencyStrength)
|
const materialResponse = getGlassMaterialResponse(toValue(options.appearance), transparencyStrength)
|
||||||
resources.uniforms.uBackgroundVisibility.value = materialResponse.backgroundVisibility
|
resources.uniforms.uBackgroundVisibility.value = materialResponse.backgroundVisibility
|
||||||
|
|||||||
@@ -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.',
|
'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.',
|
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.',
|
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',
|
glassPreset: 'Preset',
|
||||||
glassPresetNatural: 'Natural',
|
glassPresetNatural: 'Natural',
|
||||||
glassPresetGlide: 'Glide',
|
glassPresetGlide: 'Glide',
|
||||||
@@ -172,10 +173,10 @@ export default {
|
|||||||
glassReflectionStrength: 'Reflection Brightness',
|
glassReflectionStrength: 'Reflection Brightness',
|
||||||
glassTransmissionStrength: 'Transmission Brightness',
|
glassTransmissionStrength: 'Transmission Brightness',
|
||||||
glassTransparencyStrength: 'Clarity',
|
glassTransparencyStrength: 'Clarity',
|
||||||
|
glassMaterialStrengthHint:
|
||||||
|
'Clarity controls wallpaper texture visibility. Transmission Brightness controls the background inside the glass. Reflection Brightness controls surface reflections and highlights.',
|
||||||
glassOpticalStrengthHint:
|
glassOpticalStrengthHint:
|
||||||
'Sample Translation moves the shared wallpaper. Deformation controls local bending. Flow Strength controls trail range and inertia.',
|
'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',
|
purple: 'Purple',
|
||||||
custom: 'Custom Style',
|
custom: 'Custom Style',
|
||||||
transparency: 'Transparency',
|
transparency: 'Transparency',
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export default {
|
|||||||
glassQualityCssHint: '静态 CSS 材质,资源占用最低,保留通透、透射与反射,不启用实时流动。',
|
glassQualityCssHint: '静态 CSS 材质,资源占用最低,保留通透、透射与反射,不启用实时流动。',
|
||||||
glassQualityBalancedHint: '共享实时折射,优先平衡流动反馈与 GPU 占用。',
|
glassQualityBalancedHint: '共享实时折射,优先平衡流动反馈与 GPU 占用。',
|
||||||
glassQualityHighHint: '完整时序流场、扩散细节与内容保护,GPU 占用更高。',
|
glassQualityHighHint: '完整时序流场、扩散细节与内容保护,GPU 占用更高。',
|
||||||
|
glassQualityMobileHint: '保留所选档位的材质精度;移动端不启用实时流动。',
|
||||||
glassPreset: '方案',
|
glassPreset: '方案',
|
||||||
glassPresetNatural: '自然',
|
glassPresetNatural: '自然',
|
||||||
glassPresetGlide: '滑移',
|
glassPresetGlide: '滑移',
|
||||||
@@ -169,8 +170,9 @@ export default {
|
|||||||
glassReflectionStrength: '反射亮度',
|
glassReflectionStrength: '反射亮度',
|
||||||
glassTransmissionStrength: '透射亮度',
|
glassTransmissionStrength: '透射亮度',
|
||||||
glassTransparencyStrength: '通透度',
|
glassTransparencyStrength: '通透度',
|
||||||
|
glassMaterialStrengthHint:
|
||||||
|
'通透度控制壁纸纹理的可见程度,透射亮度控制玻璃内的背景明暗,反射亮度控制表面反射与高光强度。',
|
||||||
glassOpticalStrengthHint: '采样平移控制整体滑移,形变强度控制局部弯曲,流动强度控制轨迹范围与惯性。',
|
glassOpticalStrengthHint: '采样平移控制整体滑移,形变强度控制局部弯曲,流动强度控制轨迹范围与惯性。',
|
||||||
glassOpticalStrengthUnavailableHint: '标准质量保留三项材质参数,切换到均衡或高质量后,可调整采样平移、形变和流动。',
|
|
||||||
purple: '幻紫',
|
purple: '幻紫',
|
||||||
custom: '附加样式',
|
custom: '附加样式',
|
||||||
transparency: '透明度',
|
transparency: '透明度',
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export default {
|
|||||||
glassQualityCssHint: '靜態 CSS 材質,資源佔用最低,保留通透、透射與反射,不啟用即時流動。',
|
glassQualityCssHint: '靜態 CSS 材質,資源佔用最低,保留通透、透射與反射,不啟用即時流動。',
|
||||||
glassQualityBalancedHint: '共享即時折射,優先平衡流動回饋與 GPU 佔用。',
|
glassQualityBalancedHint: '共享即時折射,優先平衡流動回饋與 GPU 佔用。',
|
||||||
glassQualityHighHint: '完整時序流場、擴散細節與內容保護,GPU 佔用更高。',
|
glassQualityHighHint: '完整時序流場、擴散細節與內容保護,GPU 佔用更高。',
|
||||||
|
glassQualityMobileHint: '保留所選檔位的材質精度;行動裝置不啟用即時流動。',
|
||||||
glassPreset: '方案',
|
glassPreset: '方案',
|
||||||
glassPresetNatural: '自然',
|
glassPresetNatural: '自然',
|
||||||
glassPresetGlide: '滑移',
|
glassPresetGlide: '滑移',
|
||||||
@@ -169,8 +170,9 @@ export default {
|
|||||||
glassReflectionStrength: '反射亮度',
|
glassReflectionStrength: '反射亮度',
|
||||||
glassTransmissionStrength: '透射亮度',
|
glassTransmissionStrength: '透射亮度',
|
||||||
glassTransparencyStrength: '通透度',
|
glassTransparencyStrength: '通透度',
|
||||||
|
glassMaterialStrengthHint:
|
||||||
|
'通透度控制桌布紋理的可見程度,透射亮度控制玻璃內的背景明暗,反射亮度控制表面反射與高光強度。',
|
||||||
glassOpticalStrengthHint: '採樣平移控制整體滑移,形變強度控制局部彎曲,流動強度控制軌跡範圍與慣性。',
|
glassOpticalStrengthHint: '採樣平移控制整體滑移,形變強度控制局部彎曲,流動強度控制軌跡範圍與慣性。',
|
||||||
glassOpticalStrengthUnavailableHint: '標準品質保留三項材質參數,切換到均衡或高品質後,可調整採樣平移、形變和流動。',
|
|
||||||
purple: '幻紫',
|
purple: '幻紫',
|
||||||
custom: '附加樣式',
|
custom: '附加樣式',
|
||||||
transparency: '透明度',
|
transparency: '透明度',
|
||||||
|
|||||||
Reference in New Issue
Block a user