mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-10 10:16:44 +08:00
feat(glass): unify login and app optical presentation (#591)
This commit is contained in:
+212
-49
@@ -27,8 +27,23 @@ import { getDisplayImageUrl } from '@/utils/imageUtils'
|
|||||||
import { configureApexChartsTheme } from '@/utils/apexCharts'
|
import { configureApexChartsTheme } from '@/utils/apexCharts'
|
||||||
import { useGlobalOfflineStatus, type ConnectionFailureReason } from '@/composables/useOfflineStatus'
|
import { useGlobalOfflineStatus, type ConnectionFailureReason } from '@/composables/useOfflineStatus'
|
||||||
import { useAppActivityLifecycle } from '@/composables/useAppActivityLifecycle'
|
import { useAppActivityLifecycle } from '@/composables/useAppActivityLifecycle'
|
||||||
import { commitPreloadedBackgroundRotation, preloadBackgroundRotationImages } from '@/utils/backgroundRotation'
|
import {
|
||||||
import { GLASS_OPTICAL_STRENGTH_DEFAULT } from '@/utils/glassOptics'
|
BACKGROUND_ROTATION_GRACE_MS,
|
||||||
|
commitPreloadedBackgroundRotation,
|
||||||
|
preloadBackgroundRotationImages,
|
||||||
|
preloadBackgroundSequence,
|
||||||
|
shouldAllowBackgroundRotation,
|
||||||
|
} from '@/utils/backgroundRotation'
|
||||||
|
import {
|
||||||
|
activateLoginBackgroundLayer,
|
||||||
|
createLoginBackgroundLayers,
|
||||||
|
getLoginGlassOpticalSettings,
|
||||||
|
getLoginVisualProfile,
|
||||||
|
getLoginWallpaperRequestMode,
|
||||||
|
prepareLoginBackgroundLayer,
|
||||||
|
settleLoginBackgroundLayers,
|
||||||
|
type LoginWallpaperRequestMode,
|
||||||
|
} from '@/utils/loginPresentation'
|
||||||
|
|
||||||
const LOGIN_WALLPAPER_ROUTE = '/login'
|
const LOGIN_WALLPAPER_ROUTE = '/login'
|
||||||
const BACKGROUND_CROSSFADE_DURATION_MS = 1500
|
const BACKGROUND_CROSSFADE_DURATION_MS = 1500
|
||||||
@@ -67,49 +82,88 @@ const offlineStatus = useGlobalOfflineStatus()
|
|||||||
// 全局设置store
|
// 全局设置store
|
||||||
const globalSettingsStore = useGlobalSettingsStore()
|
const globalSettingsStore = useGlobalSettingsStore()
|
||||||
|
|
||||||
// 生成背景图片key
|
|
||||||
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
|
|
||||||
|
|
||||||
// 背景图片
|
// 背景图片
|
||||||
const backgroundImages = ref<string[]>([])
|
const backgroundImages = ref<string[]>([])
|
||||||
|
const backgroundLayers = ref(createLoginBackgroundLayers())
|
||||||
const activeImageIndex = ref(0)
|
const activeImageIndex = ref(0)
|
||||||
const previousImageIndex = ref<number | null>(null)
|
const previousImageIndex = ref<number | null>(null)
|
||||||
const isBackgroundCrossfading = ref(false)
|
const isBackgroundCrossfading = ref(false)
|
||||||
const backgroundCrossfadeStartedAt = ref(0)
|
const backgroundCrossfadeStartedAt = ref(0)
|
||||||
|
const pendingOpticalBackgroundImage = ref('')
|
||||||
const { allowsDecorativeMotion, isSuspended: isRenderThrottled, state: appActivityState } = useAppActivityLifecycle()
|
const { allowsDecorativeMotion, isSuspended: isRenderThrottled, state: appActivityState } = useAppActivityLifecycle()
|
||||||
const preferredMotion = usePreferredReducedMotion()
|
const preferredMotion = usePreferredReducedMotion()
|
||||||
// 壁纸轮播同时服从应用活动状态与系统动态效果偏好。
|
const backgroundRotationGraceActive = ref(false)
|
||||||
const allowsBackgroundRotation = computed(() => allowsDecorativeMotion.value && preferredMotion.value !== 'reduce')
|
let backgroundRotationGraceTimer: number | null = null
|
||||||
|
// 壁纸时钟允许短时后台续跑;指针、滚动和流场仍服从更严格的应用活动状态。
|
||||||
|
const allowsBackgroundRotation = computed(() =>
|
||||||
|
shouldAllowBackgroundRotation(
|
||||||
|
appActivityState.value,
|
||||||
|
backgroundRotationGraceActive.value,
|
||||||
|
preferredMotion.value === 'reduce',
|
||||||
|
),
|
||||||
|
)
|
||||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||||
const isGlassTheme = computed(() => globalTheme.name.value === 'glass')
|
const isGlassTheme = computed(() => globalTheme.name.value === 'glass')
|
||||||
const effectiveGlassSettings = useEffectiveGlassSettings()
|
const effectiveGlassSettings = useEffectiveGlassSettings()
|
||||||
const isInitialRouteReady = ref(false)
|
const isInitialRouteReady = ref(false)
|
||||||
const isBackdropTheme = computed(() => isTransparentTheme.value || isGlassTheme.value)
|
const isBackdropTheme = computed(() => isTransparentTheme.value || isGlassTheme.value)
|
||||||
const isLoginWallpaperRoute = computed(() => !isLogin.value && route.path === LOGIN_WALLPAPER_ROUTE)
|
const isLoginWallpaperRoute = computed(() => !isLogin.value && route.path === LOGIN_WALLPAPER_ROUTE)
|
||||||
// 登录专题保持固定光学参数,避免全局用户偏好改变其独立视觉合成。
|
const loginVisualProfile = computed(() => getLoginVisualProfile(globalTheme.name.value))
|
||||||
|
const loginGlassSettings = computed(() =>
|
||||||
|
getLoginGlassOpticalSettings({
|
||||||
|
appearance: effectiveGlassSettings.value.glassAppearance,
|
||||||
|
deformationStrength: effectiveGlassSettings.value.glassDeformationStrength,
|
||||||
|
flowStrength: effectiveGlassSettings.value.glassFlowStrength,
|
||||||
|
preset: effectiveGlassSettings.value.glassPreset,
|
||||||
|
reflectionStrength: effectiveGlassSettings.value.glassReflectionStrength,
|
||||||
|
transmissionStrength: effectiveGlassSettings.value.glassTransmissionStrength,
|
||||||
|
translationStrength: effectiveGlassSettings.value.glassTranslationStrength,
|
||||||
|
transparencyStrength: effectiveGlassSettings.value.glassTransparencyStrength,
|
||||||
|
}),
|
||||||
|
)
|
||||||
const opticalDeformationStrength = computed(() =>
|
const opticalDeformationStrength = computed(() =>
|
||||||
isLoginWallpaperRoute.value ? GLASS_OPTICAL_STRENGTH_DEFAULT : effectiveGlassSettings.value.glassDeformationStrength,
|
isLoginWallpaperRoute.value
|
||||||
|
? loginGlassSettings.value.deformationStrength
|
||||||
|
: effectiveGlassSettings.value.glassDeformationStrength,
|
||||||
)
|
)
|
||||||
const opticalFlowStrength = computed(() =>
|
const opticalFlowStrength = computed(() =>
|
||||||
isLoginWallpaperRoute.value ? GLASS_OPTICAL_STRENGTH_DEFAULT : effectiveGlassSettings.value.glassFlowStrength,
|
isLoginWallpaperRoute.value ? loginGlassSettings.value.flowStrength : effectiveGlassSettings.value.glassFlowStrength,
|
||||||
|
)
|
||||||
|
const opticalQuality = computed(() =>
|
||||||
|
isLoginWallpaperRoute.value ? loginGlassSettings.value.quality : effectiveGlassSettings.value.glassQuality,
|
||||||
)
|
)
|
||||||
const opticalReflectionStrength = computed(() =>
|
const opticalReflectionStrength = computed(() =>
|
||||||
isLoginWallpaperRoute.value ? GLASS_OPTICAL_STRENGTH_DEFAULT : effectiveGlassSettings.value.glassReflectionStrength,
|
isLoginWallpaperRoute.value
|
||||||
|
? loginGlassSettings.value.reflectionStrength
|
||||||
|
: effectiveGlassSettings.value.glassReflectionStrength,
|
||||||
)
|
)
|
||||||
const opticalTransparencyStrength = computed(() =>
|
const opticalTransparencyStrength = computed(() =>
|
||||||
isLoginWallpaperRoute.value ? GLASS_OPTICAL_STRENGTH_DEFAULT : effectiveGlassSettings.value.glassTransparencyStrength,
|
isLoginWallpaperRoute.value
|
||||||
|
? loginGlassSettings.value.transparencyStrength
|
||||||
|
: effectiveGlassSettings.value.glassTransparencyStrength,
|
||||||
)
|
)
|
||||||
const opticalTranslationStrength = computed(() =>
|
const opticalTranslationStrength = computed(() =>
|
||||||
isLoginWallpaperRoute.value ? GLASS_OPTICAL_STRENGTH_DEFAULT : effectiveGlassSettings.value.glassTranslationStrength,
|
isLoginWallpaperRoute.value
|
||||||
|
? loginGlassSettings.value.translationStrength
|
||||||
|
: effectiveGlassSettings.value.glassTranslationStrength,
|
||||||
|
)
|
||||||
|
const opticalTransmissionStrength = computed(() =>
|
||||||
|
isLoginWallpaperRoute.value
|
||||||
|
? loginGlassSettings.value.transmissionStrength
|
||||||
|
: effectiveGlassSettings.value.glassTransmissionStrength,
|
||||||
|
)
|
||||||
|
const shouldUseTransparentBackgroundTreatment = computed(() => isTransparentTheme.value && Boolean(isLogin.value))
|
||||||
|
const shouldUseGlassBackgroundTreatment = computed(
|
||||||
|
() => isGlassTheme.value && (Boolean(isLogin.value) || isLoginWallpaperRoute.value),
|
||||||
)
|
)
|
||||||
const shouldUseTransparentBackgroundTreatment = computed(() => Boolean(isLogin.value) && isTransparentTheme.value)
|
|
||||||
const shouldUseGlassBackgroundTreatment = computed(() => Boolean(isLogin.value) && isGlassTheme.value)
|
|
||||||
const shouldLoadBackgroundImages = computed(
|
const shouldLoadBackgroundImages = computed(
|
||||||
() => isLoginWallpaperRoute.value || (Boolean(isLogin.value) && isBackdropTheme.value),
|
() => isLoginWallpaperRoute.value || (Boolean(isLogin.value) && isBackdropTheme.value),
|
||||||
)
|
)
|
||||||
|
const wallpaperRequestMode = computed(() => getLoginWallpaperRequestMode(loginVisualProfile.value))
|
||||||
const activeBackgroundImage = computed(() => backgroundImages.value[activeImageIndex.value] ?? '')
|
const activeBackgroundImage = computed(() => backgroundImages.value[activeImageIndex.value] ?? '')
|
||||||
|
const renderedBackgroundLayers = computed(() => backgroundLayers.value)
|
||||||
const getOpticalBackgroundImage = (imageUrl: string) => getDisplayImageUrl(imageUrl, Boolean(isLogin.value))
|
const getOpticalBackgroundImage = (imageUrl: string) => getDisplayImageUrl(imageUrl, Boolean(isLogin.value))
|
||||||
// 登录页的光学层只绘制程序化焦散,不会读取该跨域壁纸;登录后才通过同源缓存采样纹理。
|
// 玻璃 profile 使用同源 catalog;旧后端返回外链时仍由 renderer 的纹理失败路径安全回退。
|
||||||
const activeOpticalBackgroundImage = computed(() => getOpticalBackgroundImage(activeBackgroundImage.value))
|
const activeOpticalBackgroundImage = computed(() => getOpticalBackgroundImage(activeBackgroundImage.value))
|
||||||
const previousOpticalBackgroundImage = computed(() => {
|
const previousOpticalBackgroundImage = computed(() => {
|
||||||
const previousIndex = previousImageIndex.value
|
const previousIndex = previousImageIndex.value
|
||||||
@@ -117,13 +171,10 @@ const previousOpticalBackgroundImage = computed(() => {
|
|||||||
|
|
||||||
return getOpticalBackgroundImage(backgroundImages.value[previousIndex] ?? '')
|
return getOpticalBackgroundImage(backgroundImages.value[previousIndex] ?? '')
|
||||||
})
|
})
|
||||||
const appWrapperStyle = computed(() => ({
|
|
||||||
'--login-wallpaper-image': activeBackgroundImage.value ? `url("${activeBackgroundImage.value}")` : 'none',
|
|
||||||
}))
|
|
||||||
const shouldRenderGlassOpticalLayer = computed(
|
const shouldRenderGlassOpticalLayer = computed(
|
||||||
() =>
|
() =>
|
||||||
isGlassTheme.value &&
|
isGlassTheme.value &&
|
||||||
effectiveGlassSettings.value.glassQuality !== 'css' &&
|
opticalQuality.value !== 'css' &&
|
||||||
isInitialRouteReady.value &&
|
isInitialRouteReady.value &&
|
||||||
Boolean(activeBackgroundImage.value),
|
Boolean(activeBackgroundImage.value),
|
||||||
)
|
)
|
||||||
@@ -141,8 +192,11 @@ const shouldRenderGlobalBlurLayer = computed(
|
|||||||
let backgroundRetryTimer: number | null = null
|
let backgroundRetryTimer: number | null = null
|
||||||
let backgroundRequestController: AbortController | null = null
|
let backgroundRequestController: AbortController | null = null
|
||||||
let backgroundCrossfadeTimer: number | null = null
|
let backgroundCrossfadeTimer: number | null = null
|
||||||
|
let pendingOpticalWallpaperTimer: number | null = null
|
||||||
|
let pendingOpticalWallpaperResolve: ((ready: boolean) => void) | null = null
|
||||||
let authenticatedStateTimer: number | null = null
|
let authenticatedStateTimer: number | null = null
|
||||||
let backgroundRotationVersion = 0
|
let backgroundRotationVersion = 0
|
||||||
|
let backgroundPreloadVersion = 0
|
||||||
|
|
||||||
// 读取并同步透明主题背景设置到根组件响应式状态。
|
// 读取并同步透明主题背景设置到根组件响应式状态。
|
||||||
function applyTransparentBackgroundSettings() {
|
function applyTransparentBackgroundSettings() {
|
||||||
@@ -367,12 +421,44 @@ function clearBackgroundCrossfadeTimer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 结束当前 GPU 纹理预备等待,旧请求不得继续提交壁纸切换。 */
|
||||||
|
function settlePendingOpticalWallpaper(ready: boolean) {
|
||||||
|
if (pendingOpticalWallpaperTimer !== null) {
|
||||||
|
window.clearTimeout(pendingOpticalWallpaperTimer)
|
||||||
|
pendingOpticalWallpaperTimer = null
|
||||||
|
}
|
||||||
|
pendingOpticalBackgroundImage.value = ''
|
||||||
|
pendingOpticalWallpaperResolve?.(ready)
|
||||||
|
pendingOpticalWallpaperResolve = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等待两个 WebGL 呈现 context 完成下一张纹理上传。 */
|
||||||
|
function prepareOpticalWallpaper(url: string) {
|
||||||
|
if (!shouldRenderGlassOpticalLayer.value || !url || url === activeOpticalBackgroundImage.value) {
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
settlePendingOpticalWallpaper(false)
|
||||||
|
pendingOpticalBackgroundImage.value = url
|
||||||
|
|
||||||
|
return new Promise<boolean>(resolve => {
|
||||||
|
pendingOpticalWallpaperResolve = resolve
|
||||||
|
pendingOpticalWallpaperTimer = window.setTimeout(() => settlePendingOpticalWallpaper(false), 10000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 只接受当前待切换 URL 的 renderer 就绪回执。 */
|
||||||
|
function handleOpticalWallpaperPrepared(url: string) {
|
||||||
|
if (url === pendingOpticalBackgroundImage.value) settlePendingOpticalWallpaper(true)
|
||||||
|
}
|
||||||
|
|
||||||
// 重置背景图交叉淡入淡出状态。
|
// 重置背景图交叉淡入淡出状态。
|
||||||
function resetBackgroundCrossfade() {
|
function resetBackgroundCrossfade() {
|
||||||
clearBackgroundCrossfadeTimer()
|
clearBackgroundCrossfadeTimer()
|
||||||
previousImageIndex.value = null
|
previousImageIndex.value = null
|
||||||
isBackgroundCrossfading.value = false
|
isBackgroundCrossfading.value = false
|
||||||
backgroundCrossfadeStartedAt.value = 0
|
backgroundCrossfadeStartedAt.value = 0
|
||||||
|
backgroundLayers.value = createLoginBackgroundLayers(activeBackgroundImage.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换期保留上一张背景的渲染状态,避免图片合成层重建时露出透明底。
|
// 切换期保留上一张背景的渲染状态,避免图片合成层重建时露出透明底。
|
||||||
@@ -380,27 +466,31 @@ function activateBackgroundImage(nextIndex: number) {
|
|||||||
if (nextIndex === activeImageIndex.value) return
|
if (nextIndex === activeImageIndex.value) return
|
||||||
|
|
||||||
clearBackgroundCrossfadeTimer()
|
clearBackgroundCrossfadeTimer()
|
||||||
|
backgroundLayers.value = prepareLoginBackgroundLayer(backgroundLayers.value, backgroundImages.value[nextIndex] ?? '')
|
||||||
previousImageIndex.value = activeImageIndex.value
|
previousImageIndex.value = activeImageIndex.value
|
||||||
isBackgroundCrossfading.value = true
|
isBackgroundCrossfading.value = true
|
||||||
backgroundCrossfadeStartedAt.value = performance.now()
|
backgroundCrossfadeStartedAt.value = performance.now()
|
||||||
activeImageIndex.value = nextIndex
|
activeImageIndex.value = nextIndex
|
||||||
|
backgroundLayers.value = activateLoginBackgroundLayer(backgroundLayers.value)
|
||||||
backgroundCrossfadeTimer = window.setTimeout(() => {
|
backgroundCrossfadeTimer = window.setTimeout(() => {
|
||||||
previousImageIndex.value = null
|
previousImageIndex.value = null
|
||||||
isBackgroundCrossfading.value = false
|
isBackgroundCrossfading.value = false
|
||||||
|
backgroundLayers.value = settleLoginBackgroundLayers(backgroundLayers.value)
|
||||||
backgroundCrossfadeTimer = null
|
backgroundCrossfadeTimer = null
|
||||||
}, BACKGROUND_CROSSFADE_DURATION_MS)
|
}, BACKGROUND_CROSSFADE_DURATION_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取背景图片
|
// 获取背景图片
|
||||||
async function fetchBackgroundImages() {
|
async function fetchBackgroundImages(requestMode: LoginWallpaperRequestMode) {
|
||||||
try {
|
try {
|
||||||
backgroundRequestController?.abort()
|
backgroundRequestController?.abort()
|
||||||
backgroundRequestController = new AbortController()
|
backgroundRequestController = new AbortController()
|
||||||
backgroundImages.value = await api.get(`/login/wallpapers`, {
|
backgroundImages.value = await api.get(`/login/wallpapers`, {
|
||||||
|
params: requestMode === 'same-origin' ? { same_origin: true } : undefined,
|
||||||
signal: backgroundRequestController.signal,
|
signal: backgroundRequestController.signal,
|
||||||
})
|
})
|
||||||
resetBackgroundCrossfade()
|
|
||||||
activeImageIndex.value = 0
|
activeImageIndex.value = 0
|
||||||
|
resetBackgroundCrossfade()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@@ -415,10 +505,7 @@ function rotateBackgroundImage() {
|
|||||||
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||||
const requestVersion = ++backgroundRotationVersion
|
const requestVersion = ++backgroundRotationVersion
|
||||||
const nextImage = backgroundImages.value[nextIndex]
|
const nextImage = backgroundImages.value[nextIndex]
|
||||||
const opticalImage =
|
const opticalImage = shouldRenderGlassOpticalLayer.value ? getOpticalBackgroundImage(nextImage) : undefined
|
||||||
shouldRenderGlassOpticalLayer.value && !isLoginWallpaperRoute.value
|
|
||||||
? getOpticalBackgroundImage(nextImage)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
void commitPreloadedBackgroundRotation({
|
void commitPreloadedBackgroundRotation({
|
||||||
canCommit: () => allowsBackgroundRotation.value && requestVersion === backgroundRotationVersion,
|
canCommit: () => allowsBackgroundRotation.value && requestVersion === backgroundRotationVersion,
|
||||||
@@ -428,15 +515,51 @@ function rotateBackgroundImage() {
|
|||||||
displayUrl: nextImage,
|
displayUrl: nextImage,
|
||||||
opticalUrl: opticalImage,
|
opticalUrl: opticalImage,
|
||||||
preload: preloadImage,
|
preload: preloadImage,
|
||||||
}),
|
}).then(imagesReady => (imagesReady && opticalImage ? prepareOpticalWallpaper(opticalImage) : imagesReady)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 当前图稳定后按轮播顺序串行预加载其余壁纸,队列失效时不再发起新请求。 */
|
||||||
|
async function preloadRemainingBackgroundImages() {
|
||||||
|
if (backgroundImages.value.length <= 1) return
|
||||||
|
|
||||||
|
const version = ++backgroundPreloadVersion
|
||||||
|
const orderedImages = backgroundImages.value
|
||||||
|
.slice(activeImageIndex.value + 1)
|
||||||
|
.concat(backgroundImages.value.slice(0, activeImageIndex.value))
|
||||||
|
|
||||||
|
await preloadBackgroundSequence({
|
||||||
|
canContinue: () => version === backgroundPreloadVersion && shouldLoadBackgroundImages.value,
|
||||||
|
preload: preloadImage,
|
||||||
|
urls: orderedImages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 停止轮询并使已经发起的壁纸预加载失效,避免非活动状态收到迟到提交。
|
// 停止轮询并使已经发起的壁纸预加载失效,避免非活动状态收到迟到提交。
|
||||||
function stopBackgroundRotation() {
|
function stopBackgroundRotation() {
|
||||||
backgroundRotationVersion += 1
|
backgroundRotationVersion += 1
|
||||||
|
backgroundPreloadVersion += 1
|
||||||
removeBackgroundTimer('background-rotation')
|
removeBackgroundTimer('background-rotation')
|
||||||
|
settlePendingOpticalWallpaper(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBackgroundRotationGrace() {
|
||||||
|
if (backgroundRotationGraceTimer !== null) {
|
||||||
|
window.clearTimeout(backgroundRotationGraceTimer)
|
||||||
|
backgroundRotationGraceTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startBackgroundRotationGrace() {
|
||||||
|
if (backgroundRotationGraceActive.value) return
|
||||||
|
|
||||||
|
backgroundRotationGraceActive.value = true
|
||||||
|
clearBackgroundRotationGrace()
|
||||||
|
backgroundRotationGraceTimer = window.setTimeout(() => {
|
||||||
|
backgroundRotationGraceTimer = null
|
||||||
|
backgroundRotationGraceActive.value = false
|
||||||
|
}, BACKGROUND_ROTATION_GRACE_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始背景图片轮换
|
// 开始背景图片轮换
|
||||||
@@ -444,19 +567,41 @@ function startBackgroundRotation() {
|
|||||||
stopBackgroundRotation()
|
stopBackgroundRotation()
|
||||||
|
|
||||||
if (allowsBackgroundRotation.value && backgroundImages.value.length > 1) {
|
if (allowsBackgroundRotation.value && backgroundImages.value.length > 1) {
|
||||||
// 使用优化的定时器管理器,后台时自动暂停
|
// 宽限期结束会使预载队列失效;恢复活动状态时从当前图继续补齐剩余壁纸。
|
||||||
|
void preloadRemainingBackgroundImages()
|
||||||
|
// 隐藏页面也允许在有界宽限期内轮换,回调自身会再次核对生命周期。
|
||||||
addBackgroundTimer(
|
addBackgroundTimer(
|
||||||
'background-rotation',
|
'background-rotation',
|
||||||
rotateBackgroundImage,
|
rotateBackgroundImage,
|
||||||
10000, // 每10秒切换一次
|
10000, // 每10秒切换一次
|
||||||
{
|
{
|
||||||
runInBackground: false, // 后台时不运行
|
runInBackground: true,
|
||||||
skipInitialRun: true, // 不需要立即执行
|
skipInitialRun: true, // 不需要立即执行
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
appActivityState,
|
||||||
|
(state, previousState) => {
|
||||||
|
if (state === 'active') {
|
||||||
|
clearBackgroundRotationGrace()
|
||||||
|
backgroundRotationGraceActive.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'idle') {
|
||||||
|
clearBackgroundRotationGrace()
|
||||||
|
backgroundRotationGraceActive.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousState === 'active') startBackgroundRotationGrace()
|
||||||
|
},
|
||||||
|
{ flush: 'sync' },
|
||||||
|
)
|
||||||
|
|
||||||
watch(allowsBackgroundRotation, allowsRotation => {
|
watch(allowsBackgroundRotation, allowsRotation => {
|
||||||
resetBackgroundCrossfade()
|
resetBackgroundCrossfade()
|
||||||
|
|
||||||
@@ -565,10 +710,12 @@ async function removeLoadingWithStateCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载背景图片
|
// 加载背景图片
|
||||||
async function loadBackgroundImages(retryCount = 0) {
|
async function loadBackgroundImages(requestMode: LoginWallpaperRequestMode, retryCount = 0) {
|
||||||
const maxRetries = 3
|
const maxRetries = 3
|
||||||
try {
|
try {
|
||||||
await fetchBackgroundImages()
|
await fetchBackgroundImages(requestMode)
|
||||||
|
const activeImage = activeBackgroundImage.value
|
||||||
|
if (activeImage && !(await preloadImage(activeImage))) throw new Error('登录壁纸首图预加载失败')
|
||||||
startBackgroundRotation()
|
startBackgroundRotation()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const isAbortError = error.name === 'AbortError' || error.code === 'ERR_CANCELED'
|
const isAbortError = error.name === 'AbortError' || error.code === 'ERR_CANCELED'
|
||||||
@@ -577,7 +724,7 @@ async function loadBackgroundImages(retryCount = 0) {
|
|||||||
const retryDelay = Math.min(baseDelay * Math.pow(2, retryCount), 10000)
|
const retryDelay = Math.min(baseDelay * Math.pow(2, retryCount), 10000)
|
||||||
backgroundRetryTimer = window.setTimeout(() => {
|
backgroundRetryTimer = window.setTimeout(() => {
|
||||||
backgroundRetryTimer = null
|
backgroundRetryTimer = null
|
||||||
loadBackgroundImages(retryCount + 1)
|
loadBackgroundImages(requestMode, retryCount + 1)
|
||||||
}, retryDelay)
|
}, retryDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,13 +768,13 @@ onMounted(async () => {
|
|||||||
window.addEventListener('focus', handlePageShowThemeSync)
|
window.addEventListener('focus', handlePageShowThemeSync)
|
||||||
window.addEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
window.addEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
||||||
|
|
||||||
// 登录页壁纸仅在未登录登录页需要,避免其他首屏额外发起图片列表请求。
|
// 背景范围或玻璃同源能力变化时重新加载;登录前后玻璃主题保持同一请求模式和活动壁纸。
|
||||||
watch(
|
watch(
|
||||||
shouldLoadBackgroundImages,
|
() => [shouldLoadBackgroundImages.value, wallpaperRequestMode.value] as const,
|
||||||
shouldLoad => {
|
([shouldLoad, requestMode]) => {
|
||||||
stopBackgroundLoading()
|
stopBackgroundLoading()
|
||||||
if (shouldLoad) {
|
if (shouldLoad) {
|
||||||
loadBackgroundImages()
|
loadBackgroundImages(requestMode)
|
||||||
} else if (!isBackdropTheme.value) {
|
} else if (!isBackdropTheme.value) {
|
||||||
backgroundImages.value = []
|
backgroundImages.value = []
|
||||||
}
|
}
|
||||||
@@ -663,6 +810,7 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 清除背景轮换定时器
|
// 清除背景轮换定时器
|
||||||
stopBackgroundLoading()
|
stopBackgroundLoading()
|
||||||
|
clearBackgroundRotationGrace()
|
||||||
if (authenticatedStateTimer) {
|
if (authenticatedStateTimer) {
|
||||||
window.clearTimeout(authenticatedStateTimer)
|
window.clearTimeout(authenticatedStateTimer)
|
||||||
authenticatedStateTimer = null
|
authenticatedStateTimer = null
|
||||||
@@ -684,10 +832,11 @@ onUnmounted(() => {
|
|||||||
:class="{
|
:class="{
|
||||||
'app-wrapper--background-transition': isBackgroundCrossfading,
|
'app-wrapper--background-transition': isBackgroundCrossfading,
|
||||||
'app-wrapper--decorative-motion-paused': !allowsDecorativeMotion,
|
'app-wrapper--decorative-motion-paused': !allowsDecorativeMotion,
|
||||||
|
'app-wrapper--login-glass-high': isLoginWallpaperRoute && loginVisualProfile === 'glass',
|
||||||
|
'app-wrapper--login-wallpaper': isLoginWallpaperRoute,
|
||||||
'app-wrapper--render-throttled': isRenderThrottled,
|
'app-wrapper--render-throttled': isRenderThrottled,
|
||||||
}"
|
}"
|
||||||
:data-app-activity-state="appActivityState"
|
:data-app-activity-state="appActivityState"
|
||||||
:style="appWrapperStyle"
|
|
||||||
>
|
>
|
||||||
<!-- 登录页、透明主题和玻璃主题共用动态壁纸场景。 -->
|
<!-- 登录页、透明主题和玻璃主题共用动态壁纸场景。 -->
|
||||||
<div
|
<div
|
||||||
@@ -701,11 +850,11 @@ onUnmounted(() => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(imageUrl, index) in backgroundImages"
|
v-for="layer in renderedBackgroundLayers"
|
||||||
:key="`bg-${index}-${loginStateKey}`"
|
:key="layer.key"
|
||||||
class="background-image"
|
class="background-image"
|
||||||
:class="{ 'active': index === activeImageIndex, 'previous': index === previousImageIndex }"
|
:class="layer.role"
|
||||||
:style="{ 'backgroundImage': `url(${imageUrl})` }"
|
:style="{ 'backgroundImage': layer.url ? `url(${layer.url})` : undefined }"
|
||||||
/>
|
/>
|
||||||
<!-- 全局磨砂层 -->
|
<!-- 全局磨砂层 -->
|
||||||
<div v-if="shouldRenderGlobalBlurLayer" class="global-blur-layer"></div>
|
<div v-if="shouldRenderGlobalBlurLayer" class="global-blur-layer"></div>
|
||||||
@@ -715,9 +864,10 @@ onUnmounted(() => {
|
|||||||
:appearance="effectiveGlassSettings.glassAppearance"
|
:appearance="effectiveGlassSettings.glassAppearance"
|
||||||
:deformation-strength="opticalDeformationStrength"
|
:deformation-strength="opticalDeformationStrength"
|
||||||
:flow-strength="opticalFlowStrength"
|
:flow-strength="opticalFlowStrength"
|
||||||
:quality="effectiveGlassSettings.glassQuality === 'high' ? 'high' : 'balanced'"
|
:quality="opticalQuality === 'high' ? 'high' : 'balanced'"
|
||||||
:reflection-strength="opticalReflectionStrength"
|
:reflection-strength="opticalReflectionStrength"
|
||||||
:transparency-strength="opticalTransparencyStrength"
|
:transparency-strength="opticalTransparencyStrength"
|
||||||
|
:transmission-strength="opticalTransmissionStrength"
|
||||||
:translation-strength="opticalTranslationStrength"
|
:translation-strength="opticalTranslationStrength"
|
||||||
:route-key="route.fullPath"
|
:route-key="route.fullPath"
|
||||||
:tint-color="globalTheme.current.value.colors.primary"
|
:tint-color="globalTheme.current.value.colors.primary"
|
||||||
@@ -725,9 +875,14 @@ onUnmounted(() => {
|
|||||||
:transition-started-at="backgroundCrossfadeStartedAt"
|
:transition-started-at="backgroundCrossfadeStartedAt"
|
||||||
:wallpaper-url="activeOpticalBackgroundImage"
|
:wallpaper-url="activeOpticalBackgroundImage"
|
||||||
:previous-wallpaper-url="previousOpticalBackgroundImage"
|
:previous-wallpaper-url="previousOpticalBackgroundImage"
|
||||||
|
:pending-wallpaper-url="pendingOpticalBackgroundImage"
|
||||||
|
@wallpaper-prepared="handleOpticalWallpaperPrepared"
|
||||||
/>
|
/>
|
||||||
<!-- 页面内容 -->
|
<!-- 页面内容 -->
|
||||||
<VApp :class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }">
|
<VApp
|
||||||
|
:class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }"
|
||||||
|
:data-login-visual-profile="isLoginWallpaperRoute ? loginVisualProfile : undefined"
|
||||||
|
>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<!-- 全局共享弹窗入口,列表与卡片按需在这里挂载业务弹窗。 -->
|
<!-- 全局共享弹窗入口,列表与卡片按需在这里挂载业务弹窗。 -->
|
||||||
<SharedDialogHost />
|
<SharedDialogHost />
|
||||||
@@ -755,6 +910,14 @@ onUnmounted(() => {
|
|||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 登录内容与壁纸进入同一文档弹性合成上下文;sticky 仍保持普通滚动时的 viewport 锁定。
|
||||||
|
.app-wrapper--login-wallpaper .background-container {
|
||||||
|
position: sticky;
|
||||||
|
block-size: 100dvh;
|
||||||
|
margin-block-end: -100dvh;
|
||||||
|
inset-block-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.background-image {
|
.background-image {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@@ -803,8 +966,8 @@ onUnmounted(() => {
|
|||||||
.background-container.is-glass-theme .background-image.active::after,
|
.background-container.is-glass-theme .background-image.active::after,
|
||||||
.background-container.is-glass-theme .background-image.previous::after {
|
.background-container.is-glass-theme .background-image.previous::after {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 50% 18%, transparent 24%, rgba(6, 10, 19, 16%) 100%),
|
radial-gradient(circle at 50% 18%, transparent 24%, rgba(6, 10, 19, 12%) 100%),
|
||||||
linear-gradient(rgba(6, 10, 19, 14%) 0%, rgba(6, 10, 19, 40%) 100%);
|
linear-gradient(rgba(6, 10, 19, 10%) 0%, rgba(6, 10, 19, 30%) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-glass-appearance='tinted'] .background-container.is-glass-theme .background-image.active,
|
html[data-glass-appearance='tinted'] .background-container.is-glass-theme .background-image.active,
|
||||||
@@ -819,8 +982,8 @@ html[data-glass-appearance='tinted'] .background-container.is-glass-theme .backg
|
|||||||
html[data-glass-appearance='tinted'] .background-container.is-glass-theme .background-image.active::after,
|
html[data-glass-appearance='tinted'] .background-container.is-glass-theme .background-image.active::after,
|
||||||
html[data-glass-appearance='tinted'] .background-container.is-glass-theme .background-image.previous::after {
|
html[data-glass-appearance='tinted'] .background-container.is-glass-theme .background-image.previous::after {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 50% 18%, transparent 22%, rgba(6, 10, 19, 18%) 100%),
|
radial-gradient(circle at 50% 18%, transparent 22%, rgba(6, 10, 19, 14%) 100%),
|
||||||
linear-gradient(rgba(6, 10, 19, 14%) 0%, rgba(6, 10, 19, 42%) 100%), rgba(var(--v-theme-primary), 3%);
|
linear-gradient(rgba(6, 10, 19, 10%) 0%, rgba(6, 10, 19, 32%) 100%), rgba(var(--v-theme-primary), 3%);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-glass-appearance='frosted'] .background-container.is-glass-theme .background-image.active,
|
html[data-glass-appearance='frosted'] .background-container.is-glass-theme .background-image.active,
|
||||||
@@ -834,7 +997,7 @@ html[data-glass-appearance='frosted'] .background-container.is-glass-theme .back
|
|||||||
|
|
||||||
html[data-glass-appearance='frosted'] .background-container.is-glass-theme .background-image.active::after,
|
html[data-glass-appearance='frosted'] .background-container.is-glass-theme .background-image.active::after,
|
||||||
html[data-glass-appearance='frosted'] .background-container.is-glass-theme .background-image.previous::after {
|
html[data-glass-appearance='frosted'] .background-container.is-glass-theme .background-image.previous::after {
|
||||||
background: linear-gradient(rgba(6, 10, 19, 30%) 0%, rgba(6, 10, 19, 58%) 100%), rgba(11, 19, 34, 10%);
|
background: linear-gradient(rgba(6, 10, 19, 24%) 0%, rgba(6, 10, 19, 48%) 100%), rgba(11, 19, 34, 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-container.is-transparent-glass-lightweight .background-image.active,
|
.background-container.is-transparent-glass-lightweight .background-image.active,
|
||||||
@@ -867,7 +1030,7 @@ html[data-glass-appearance='frosted'] .background-container.is-glass-theme .back
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-wrapper--decorative-motion-paused {
|
.app-wrapper--decorative-motion-paused {
|
||||||
.login-bg-decor *,
|
.login-ambient-light *,
|
||||||
.login-logo,
|
.login-logo,
|
||||||
.login-logo-wrapper,
|
.login-logo-wrapper,
|
||||||
.login-logo-wrapper::before,
|
.login-logo-wrapper::before,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const draftFlowStrength = ref(settings.value.glassFlowStrength)
|
|||||||
const draftPreset = ref<GlassOpticalPreset>(settings.value.glassPreset)
|
const draftPreset = ref<GlassOpticalPreset>(settings.value.glassPreset)
|
||||||
const draftQuality = ref<ThemeCustomizerGlassQuality>(settings.value.glassQuality)
|
const draftQuality = ref<ThemeCustomizerGlassQuality>(settings.value.glassQuality)
|
||||||
const draftReflectionStrength = ref(settings.value.glassReflectionStrength)
|
const draftReflectionStrength = ref(settings.value.glassReflectionStrength)
|
||||||
|
const draftTransmissionStrength = ref(settings.value.glassTransmissionStrength)
|
||||||
const draftTranslationStrength = ref(settings.value.glassTranslationStrength)
|
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)
|
||||||
@@ -69,6 +70,7 @@ watch(
|
|||||||
draftPreset.value = settings.value.glassPreset
|
draftPreset.value = settings.value.glassPreset
|
||||||
draftQuality.value = settings.value.glassQuality
|
draftQuality.value = settings.value.glassQuality
|
||||||
draftReflectionStrength.value = settings.value.glassReflectionStrength
|
draftReflectionStrength.value = settings.value.glassReflectionStrength
|
||||||
|
draftTransmissionStrength.value = settings.value.glassTransmissionStrength
|
||||||
draftTranslationStrength.value = settings.value.glassTranslationStrength
|
draftTranslationStrength.value = settings.value.glassTranslationStrength
|
||||||
draftTransparencyStrength.value = settings.value.glassTransparencyStrength
|
draftTransparencyStrength.value = settings.value.glassTransparencyStrength
|
||||||
} else if (previous) {
|
} else if (previous) {
|
||||||
@@ -122,13 +124,14 @@ function updateQuality(value: unknown) {
|
|||||||
previewGlassSettings({ glassQuality: option.value })
|
previewGlassSettings({ glassQuality: option.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将五个具体参数作为一个预览事务同步,预置只负责生成这些值。 */
|
/** 将六个具体参数作为一个预览事务同步,预置只负责生成这些值。 */
|
||||||
function previewDraftParameters() {
|
function previewDraftParameters() {
|
||||||
previewGlassSettings({
|
previewGlassSettings({
|
||||||
glassDeformationStrength: draftDeformationStrength.value,
|
glassDeformationStrength: draftDeformationStrength.value,
|
||||||
glassFlowStrength: draftFlowStrength.value,
|
glassFlowStrength: draftFlowStrength.value,
|
||||||
glassPreset: draftPreset.value,
|
glassPreset: draftPreset.value,
|
||||||
glassReflectionStrength: draftReflectionStrength.value,
|
glassReflectionStrength: draftReflectionStrength.value,
|
||||||
|
glassTransmissionStrength: draftTransmissionStrength.value,
|
||||||
glassTranslationStrength: draftTranslationStrength.value,
|
glassTranslationStrength: draftTranslationStrength.value,
|
||||||
glassTransparencyStrength: draftTransparencyStrength.value,
|
glassTransparencyStrength: draftTransparencyStrength.value,
|
||||||
})
|
})
|
||||||
@@ -144,6 +147,7 @@ function applyPreset(value: unknown) {
|
|||||||
draftDeformationStrength.value = parameters.deformation
|
draftDeformationStrength.value = parameters.deformation
|
||||||
draftFlowStrength.value = parameters.flow
|
draftFlowStrength.value = parameters.flow
|
||||||
draftReflectionStrength.value = parameters.reflection
|
draftReflectionStrength.value = parameters.reflection
|
||||||
|
draftTransmissionStrength.value = parameters.transmission
|
||||||
draftTranslationStrength.value = parameters.translation
|
draftTranslationStrength.value = parameters.translation
|
||||||
draftTransparencyStrength.value = parameters.transparency
|
draftTransparencyStrength.value = parameters.transparency
|
||||||
previewDraftParameters()
|
previewDraftParameters()
|
||||||
@@ -173,6 +177,12 @@ function updateReflectionStrength(value: unknown) {
|
|||||||
previewGlassSettings({ glassReflectionStrength: draftReflectionStrength.value })
|
previewGlassSettings({ glassReflectionStrength: draftReflectionStrength.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将透射亮度限制为稳定范围并即时调整卡片内部壁纸的明暗。 */
|
||||||
|
function updateTransmissionStrength(value: unknown) {
|
||||||
|
draftTransmissionStrength.value = normalizeGlassOpticalStrength(Array.isArray(value) ? value[0] : value)
|
||||||
|
previewGlassSettings({ glassTransmissionStrength: draftTransmissionStrength.value })
|
||||||
|
}
|
||||||
|
|
||||||
/** 将通透度限制为稳定范围并即时调整材质与真实壁纸的占比。 */
|
/** 将通透度限制为稳定范围并即时调整材质与真实壁纸的占比。 */
|
||||||
function updateTransparencyStrength(value: unknown) {
|
function updateTransparencyStrength(value: unknown) {
|
||||||
draftTransparencyStrength.value = normalizeGlassOpticalStrength(Array.isArray(value) ? value[0] : value)
|
draftTransparencyStrength.value = normalizeGlassOpticalStrength(Array.isArray(value) ? value[0] : value)
|
||||||
@@ -198,6 +208,7 @@ async function saveSettings() {
|
|||||||
glassPreset: draftPreset.value,
|
glassPreset: draftPreset.value,
|
||||||
glassQuality: draftQuality.value,
|
glassQuality: draftQuality.value,
|
||||||
glassReflectionStrength: draftReflectionStrength.value,
|
glassReflectionStrength: draftReflectionStrength.value,
|
||||||
|
glassTransmissionStrength: draftTransmissionStrength.value,
|
||||||
glassTranslationStrength: draftTranslationStrength.value,
|
glassTranslationStrength: draftTranslationStrength.value,
|
||||||
glassTransparencyStrength: draftTransparencyStrength.value,
|
glassTransparencyStrength: draftTransparencyStrength.value,
|
||||||
})
|
})
|
||||||
@@ -292,6 +303,7 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="glass-settings-dialog__tuning">
|
<section class="glass-settings-dialog__tuning">
|
||||||
|
<h3 class="glass-settings-dialog__group-label">{{ t('theme.glassMaterialTuning') }}</h3>
|
||||||
<div class="glass-settings-dialog__slider-header">
|
<div class="glass-settings-dialog__slider-header">
|
||||||
<h3 class="glass-settings-dialog__label">{{ t('theme.glassTransparencyStrength') }}</h3>
|
<h3 class="glass-settings-dialog__label">{{ t('theme.glassTransparencyStrength') }}</h3>
|
||||||
<output>{{ draftTransparencyStrength }}%</output>
|
<output>{{ draftTransparencyStrength }}%</output>
|
||||||
@@ -309,6 +321,23 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
@update:model-value="updateTransparencyStrength"
|
@update:model-value="updateTransparencyStrength"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="glass-settings-dialog__slider-header glass-settings-dialog__slider-header--spaced">
|
||||||
|
<h3 class="glass-settings-dialog__label">{{ t('theme.glassTransmissionStrength') }}</h3>
|
||||||
|
<output>{{ draftTransmissionStrength }}%</output>
|
||||||
|
</div>
|
||||||
|
<VSlider
|
||||||
|
:model-value="draftTransmissionStrength"
|
||||||
|
:aria-label="t('theme.glassTransmissionStrength')"
|
||||||
|
:min="GLASS_OPTICAL_STRENGTH_MIN"
|
||||||
|
:max="GLASS_OPTICAL_STRENGTH_MAX"
|
||||||
|
:step="1"
|
||||||
|
color="primary"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details
|
||||||
|
thumb-label
|
||||||
|
@update:model-value="updateTransmissionStrength"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="glass-settings-dialog__slider-header glass-settings-dialog__slider-header--spaced">
|
<div class="glass-settings-dialog__slider-header glass-settings-dialog__slider-header--spaced">
|
||||||
<h3 class="glass-settings-dialog__label">{{ t('theme.glassReflectionStrength') }}</h3>
|
<h3 class="glass-settings-dialog__label">{{ t('theme.glassReflectionStrength') }}</h3>
|
||||||
<output>{{ draftReflectionStrength }}%</output>
|
<output>{{ draftReflectionStrength }}%</output>
|
||||||
@@ -327,6 +356,7 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<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>
|
||||||
<div class="glass-settings-dialog__slider-header">
|
<div class="glass-settings-dialog__slider-header">
|
||||||
<h3 class="glass-settings-dialog__label">{{ t('theme.glassTranslationStrength') }}</h3>
|
<h3 class="glass-settings-dialog__label">{{ t('theme.glassTranslationStrength') }}</h3>
|
||||||
<output>{{ draftTranslationStrength }}%</output>
|
<output>{{ draftTranslationStrength }}%</output>
|
||||||
@@ -421,6 +451,14 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.glass-settings-dialog__group-label {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.glass-settings-dialog__slider-header {
|
.glass-settings-dialog__slider-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -444,7 +482,9 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.glass-settings-dialog__live-controls {
|
.glass-settings-dialog__live-controls {
|
||||||
margin-block-start: 18px;
|
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||||
|
margin-block-start: 24px;
|
||||||
|
padding-block-start: 20px;
|
||||||
|
|
||||||
:deep(.v-slider) {
|
:deep(.v-slider) {
|
||||||
margin-block-start: 4px;
|
margin-block-start: 4px;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const mocks = vi.hoisted(() => ({
|
|||||||
glassPreset: 'natural',
|
glassPreset: 'natural',
|
||||||
glassQuality: 'css',
|
glassQuality: 'css',
|
||||||
glassReflectionStrength: 50,
|
glassReflectionStrength: 50,
|
||||||
|
glassTransmissionStrength: 50,
|
||||||
glassTranslationStrength: 50,
|
glassTranslationStrength: 50,
|
||||||
glassTransparencyStrength: 50,
|
glassTransparencyStrength: 50,
|
||||||
},
|
},
|
||||||
@@ -57,6 +58,7 @@ describe('GlassSettingsDialog', () => {
|
|||||||
mocks.settings.value.glassPreset = 'natural'
|
mocks.settings.value.glassPreset = 'natural'
|
||||||
mocks.settings.value.glassQuality = 'css'
|
mocks.settings.value.glassQuality = 'css'
|
||||||
mocks.settings.value.glassReflectionStrength = 50
|
mocks.settings.value.glassReflectionStrength = 50
|
||||||
|
mocks.settings.value.glassTransmissionStrength = 50
|
||||||
mocks.settings.value.glassTranslationStrength = 50
|
mocks.settings.value.glassTranslationStrength = 50
|
||||||
mocks.settings.value.glassTransparencyStrength = 50
|
mocks.settings.value.glassTransparencyStrength = 50
|
||||||
})
|
})
|
||||||
@@ -76,7 +78,7 @@ describe('GlassSettingsDialog', () => {
|
|||||||
})
|
})
|
||||||
const sliders = wrapper.findAll('.slider-stub')
|
const sliders = wrapper.findAll('.slider-stub')
|
||||||
|
|
||||||
expect(sliders).toHaveLength(2)
|
expect(sliders).toHaveLength(3)
|
||||||
expect(sliders[0].attributes('data-disabled')).toBe('undefined')
|
expect(sliders[0].attributes('data-disabled')).toBe('undefined')
|
||||||
expect(sliders[1].attributes('data-disabled')).toBe('undefined')
|
expect(sliders[1].attributes('data-disabled')).toBe('undefined')
|
||||||
await wrapper.setProps({ modelValue: false })
|
await wrapper.setProps({ modelValue: false })
|
||||||
@@ -91,6 +93,7 @@ describe('GlassSettingsDialog', () => {
|
|||||||
mocks.settings.value.glassDeformationStrength = 65
|
mocks.settings.value.glassDeformationStrength = 65
|
||||||
mocks.settings.value.glassFlowStrength = 61
|
mocks.settings.value.glassFlowStrength = 61
|
||||||
mocks.settings.value.glassReflectionStrength = 58
|
mocks.settings.value.glassReflectionStrength = 58
|
||||||
|
mocks.settings.value.glassTransmissionStrength = 57
|
||||||
mocks.settings.value.glassTranslationStrength = 57
|
mocks.settings.value.glassTranslationStrength = 57
|
||||||
mocks.settings.value.glassTransparencyStrength = 55
|
mocks.settings.value.glassTransparencyStrength = 55
|
||||||
const wrapper = shallowMount(GlassSettingsDialog, {
|
const wrapper = shallowMount(GlassSettingsDialog, {
|
||||||
@@ -119,7 +122,8 @@ describe('GlassSettingsDialog', () => {
|
|||||||
glassDeformationStrength: 82,
|
glassDeformationStrength: 82,
|
||||||
glassFlowStrength: 80,
|
glassFlowStrength: 80,
|
||||||
glassPreset: 'liquid',
|
glassPreset: 'liquid',
|
||||||
glassReflectionStrength: 44,
|
glassReflectionStrength: 31,
|
||||||
|
glassTransmissionStrength: 58,
|
||||||
glassTranslationStrength: 54,
|
glassTranslationStrength: 54,
|
||||||
glassTransparencyStrength: 46,
|
glassTransparencyStrength: 46,
|
||||||
})
|
})
|
||||||
@@ -141,9 +145,9 @@ describe('GlassSettingsDialog', () => {
|
|||||||
})
|
})
|
||||||
const sliders = wrapper.findAll('.slider-stub')
|
const sliders = wrapper.findAll('.slider-stub')
|
||||||
|
|
||||||
expect(sliders).toHaveLength(2)
|
expect(sliders).toHaveLength(3)
|
||||||
expect(sliders[1].attributes('data-disabled')).toBe('undefined')
|
expect(sliders[2].attributes('data-disabled')).toBe('undefined')
|
||||||
await sliders[1].setValue('86')
|
await sliders[2].setValue('86')
|
||||||
|
|
||||||
expect(mocks.previewGlassSettings).toHaveBeenCalledWith({ glassReflectionStrength: 86 })
|
expect(mocks.previewGlassSettings).toHaveBeenCalledWith({ glassReflectionStrength: 86 })
|
||||||
})
|
})
|
||||||
@@ -197,7 +201,7 @@ describe('GlassSettingsDialog', () => {
|
|||||||
expect(preset.attributes('data-model-value')).toBe('glide')
|
expect(preset.attributes('data-model-value')).toBe('glide')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('normalizes and previews all five independent slider values', async () => {
|
it('normalizes and previews all six independent slider values', async () => {
|
||||||
mocks.settings.value.glassQuality = 'balanced'
|
mocks.settings.value.glassQuality = 'balanced'
|
||||||
const wrapper = shallowMount(GlassSettingsDialog, {
|
const wrapper = shallowMount(GlassSettingsDialog, {
|
||||||
global: {
|
global: {
|
||||||
@@ -213,20 +217,22 @@ describe('GlassSettingsDialog', () => {
|
|||||||
})
|
})
|
||||||
const sliders = wrapper.findAll('.slider-stub')
|
const sliders = wrapper.findAll('.slider-stub')
|
||||||
|
|
||||||
expect(sliders).toHaveLength(5)
|
expect(sliders).toHaveLength(6)
|
||||||
expect(sliders.every(slider => slider.attributes('data-disabled') !== 'true')).toBe(true)
|
expect(sliders.every(slider => slider.attributes('data-disabled') !== 'true')).toBe(true)
|
||||||
|
|
||||||
await sliders[0].setValue('91.4')
|
await sliders[0].setValue('91.4')
|
||||||
await sliders[1].setValue('73.6')
|
await sliders[1].setValue('77.5')
|
||||||
await sliders[2].setValue('84.2')
|
await sliders[2].setValue('73.6')
|
||||||
await sliders[3].setValue('68.7')
|
await sliders[3].setValue('84.2')
|
||||||
await sliders[4].setValue('62.2')
|
await sliders[4].setValue('68.7')
|
||||||
|
await sliders[5].setValue('62.2')
|
||||||
|
|
||||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(1, { glassTransparencyStrength: 91 })
|
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(1, { glassTransparencyStrength: 91 })
|
||||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(2, { glassReflectionStrength: 74 })
|
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(2, { glassTransmissionStrength: 78 })
|
||||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(3, { glassTranslationStrength: 84 })
|
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(3, { glassReflectionStrength: 74 })
|
||||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(4, { glassDeformationStrength: 69 })
|
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(4, { glassTranslationStrength: 84 })
|
||||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(5, { glassFlowStrength: 62 })
|
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(5, { glassDeformationStrength: 69 })
|
||||||
|
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(6, { glassFlowStrength: 62 })
|
||||||
expect(mocks.commitGlassPreview).not.toHaveBeenCalled()
|
expect(mocks.commitGlassPreview).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -246,9 +252,10 @@ describe('GlassSettingsDialog', () => {
|
|||||||
})
|
})
|
||||||
const sliders = wrapper.findAll('.slider-stub')
|
const sliders = wrapper.findAll('.slider-stub')
|
||||||
|
|
||||||
expect(sliders).toHaveLength(5)
|
expect(sliders).toHaveLength(6)
|
||||||
expect(sliders.map(slider => slider.attributes('aria-label'))).toEqual([
|
expect(sliders.map(slider => slider.attributes('aria-label'))).toEqual([
|
||||||
'theme.glassTransparencyStrength',
|
'theme.glassTransparencyStrength',
|
||||||
|
'theme.glassTransmissionStrength',
|
||||||
'theme.glassReflectionStrength',
|
'theme.glassReflectionStrength',
|
||||||
'theme.glassTranslationStrength',
|
'theme.glassTranslationStrength',
|
||||||
'theme.glassDeformationStrength',
|
'theme.glassDeformationStrength',
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const props = defineProps<{
|
|||||||
reflectionStrength: number
|
reflectionStrength: number
|
||||||
/** 用户选择的真实壁纸可见度。 */
|
/** 用户选择的真实壁纸可见度。 */
|
||||||
transparencyStrength: number
|
transparencyStrength: number
|
||||||
|
/** 玻璃内部壁纸采样的透射亮度;不会改变外层壁纸的曝光合同。 */
|
||||||
|
transmissionStrength: number
|
||||||
/** 用户选择的共享壁纸采样平移强度。 */
|
/** 用户选择的共享壁纸采样平移强度。 */
|
||||||
translationStrength: number
|
translationStrength: number
|
||||||
/** 路由变化标识,用于在页面内容稳定后重新发现高价值表面。 */
|
/** 路由变化标识,用于在页面内容稳定后重新发现高价值表面。 */
|
||||||
@@ -34,6 +36,12 @@ const props = defineProps<{
|
|||||||
wallpaperUrl: string
|
wallpaperUrl: string
|
||||||
/** 切换期保留的上一张壁纸;空值表示当前没有交叉淡化。 */
|
/** 切换期保留的上一张壁纸;空值表示当前没有交叉淡化。 */
|
||||||
previousWallpaperUrl: string
|
previousWallpaperUrl: string
|
||||||
|
/** 下一张同源壁纸;两个 context 均完成上传后才允许外层提交切换。 */
|
||||||
|
pendingWallpaperUrl?: string
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** fixed 与 scroll renderer 均已准备同一张待切换纹理。 */
|
||||||
|
wallpaperPrepared: [url: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const fixedCanvas = ref<HTMLCanvasElement | null>(null)
|
const fixedCanvas = ref<HTMLCanvasElement | null>(null)
|
||||||
@@ -49,6 +57,7 @@ const fixedRenderer = useGlassOpticalRenderer({
|
|||||||
quality: () => props.quality,
|
quality: () => props.quality,
|
||||||
reflectionStrength: () => props.reflectionStrength,
|
reflectionStrength: () => props.reflectionStrength,
|
||||||
transparencyStrength: () => props.transparencyStrength,
|
transparencyStrength: () => props.transparencyStrength,
|
||||||
|
transmissionStrength: () => props.transmissionStrength,
|
||||||
translationStrength: () => props.translationStrength,
|
translationStrength: () => props.translationStrength,
|
||||||
routeKey: () => props.routeKey,
|
routeKey: () => props.routeKey,
|
||||||
tintColor: () => props.tintColor,
|
tintColor: () => props.tintColor,
|
||||||
@@ -56,6 +65,7 @@ const fixedRenderer = useGlassOpticalRenderer({
|
|||||||
transitionStartedAt: () => props.transitionStartedAt,
|
transitionStartedAt: () => props.transitionStartedAt,
|
||||||
wallpaperUrl: () => props.wallpaperUrl,
|
wallpaperUrl: () => props.wallpaperUrl,
|
||||||
previousWallpaperUrl: () => props.previousWallpaperUrl,
|
previousWallpaperUrl: () => props.previousWallpaperUrl,
|
||||||
|
pendingWallpaperUrl: () => props.pendingWallpaperUrl ?? '',
|
||||||
surfaceSpace: 'fixed',
|
surfaceSpace: 'fixed',
|
||||||
syncDocumentState: false,
|
syncDocumentState: false,
|
||||||
})
|
})
|
||||||
@@ -69,6 +79,7 @@ const scrollRenderer = useGlassOpticalRenderer({
|
|||||||
quality: () => props.quality,
|
quality: () => props.quality,
|
||||||
reflectionStrength: () => props.reflectionStrength,
|
reflectionStrength: () => props.reflectionStrength,
|
||||||
transparencyStrength: () => props.transparencyStrength,
|
transparencyStrength: () => props.transparencyStrength,
|
||||||
|
transmissionStrength: () => props.transmissionStrength,
|
||||||
translationStrength: () => props.translationStrength,
|
translationStrength: () => props.translationStrength,
|
||||||
routeKey: () => props.routeKey,
|
routeKey: () => props.routeKey,
|
||||||
tintColor: () => props.tintColor,
|
tintColor: () => props.tintColor,
|
||||||
@@ -76,6 +87,7 @@ const scrollRenderer = useGlassOpticalRenderer({
|
|||||||
transitionStartedAt: () => props.transitionStartedAt,
|
transitionStartedAt: () => props.transitionStartedAt,
|
||||||
wallpaperUrl: () => props.wallpaperUrl,
|
wallpaperUrl: () => props.wallpaperUrl,
|
||||||
previousWallpaperUrl: () => props.previousWallpaperUrl,
|
previousWallpaperUrl: () => props.previousWallpaperUrl,
|
||||||
|
pendingWallpaperUrl: () => props.pendingWallpaperUrl ?? '',
|
||||||
surfaceSpace: 'scroll',
|
surfaceSpace: 'scroll',
|
||||||
syncDocumentState: false,
|
syncDocumentState: false,
|
||||||
})
|
})
|
||||||
@@ -94,6 +106,13 @@ watchEffect(() => {
|
|||||||
setGlassRendererState(rendererState, state)
|
setGlassRendererState(rendererState, state)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const url = props.pendingWallpaperUrl
|
||||||
|
if (url && fixedRenderer.preparedWallpaperUrl.value === url && scrollRenderer.preparedWallpaperUrl.value === url) {
|
||||||
|
emit('wallpaperPrepared', url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onScopeDispose(() => {
|
onScopeDispose(() => {
|
||||||
setGlassRendererState(rendererState, 'fallback')
|
setGlassRendererState(rendererState, 'fallback')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import { GLASS_OPTICAL_STRENGTH_DEFAULT } from '@/utils/glassOptics'
|
import { GLASS_OPTICAL_STRENGTH_DEFAULT, getGlassOpticalPresetParameters } from '@/utils/glassOptics'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'close': []
|
'close': []
|
||||||
@@ -38,6 +38,7 @@ const { appMode } = usePWA()
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { global: globalTheme } = useTheme()
|
const { global: globalTheme } = useTheme()
|
||||||
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
||||||
|
const defaultGlassTransmissionStrength = getGlassOpticalPresetParameters('clear', 'balanced', 'natural').transmission
|
||||||
|
|
||||||
// 将主题定制器打开状态同步到根节点,供全局悬浮按钮避让右侧面板。
|
// 将主题定制器打开状态同步到根节点,供全局悬浮按钮避让右侧面板。
|
||||||
function syncThemeCustomizerOpenState(isOpen: boolean) {
|
function syncThemeCustomizerOpenState(isOpen: boolean) {
|
||||||
@@ -145,6 +146,7 @@ const hasAppModeCustomization = computed(() => {
|
|||||||
settings.value.glassFlowStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
settings.value.glassFlowStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
||||||
settings.value.glassQuality !== 'css' ||
|
settings.value.glassQuality !== 'css' ||
|
||||||
settings.value.glassReflectionStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
settings.value.glassReflectionStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
||||||
|
settings.value.glassTransmissionStrength !== defaultGlassTransmissionStrength ||
|
||||||
settings.value.glassTranslationStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
settings.value.glassTranslationStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
||||||
settings.value.glassTransparencyStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
settings.value.glassTransparencyStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
||||||
settings.value.radius !== 'default' ||
|
settings.value.radius !== 'default' ||
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ describe('GlassOpticalLayer', () => {
|
|||||||
tintColor: '#8D51F9',
|
tintColor: '#8D51F9',
|
||||||
transitionDuration: 1500,
|
transitionDuration: 1500,
|
||||||
transitionStartedAt: 0,
|
transitionStartedAt: 0,
|
||||||
|
transmissionStrength: 84,
|
||||||
translationStrength: 50,
|
translationStrength: 50,
|
||||||
transparencyStrength: 50,
|
transparencyStrength: 50,
|
||||||
wallpaperUrl: '/wallpaper.jpg',
|
wallpaperUrl: '/wallpaper.jpg',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
collectGlassOpticalRects,
|
collectGlassOpticalRects,
|
||||||
containsGlassOpticalSurface,
|
containsGlassOpticalSurface,
|
||||||
setGlassRendererState,
|
setGlassRendererState,
|
||||||
|
useGlassOpticalInteractionSource,
|
||||||
useGlassOpticalRenderer,
|
useGlassOpticalRenderer,
|
||||||
type GlassRendererState,
|
type GlassRendererState,
|
||||||
} from '@/composables/useGlassOpticalRenderer'
|
} from '@/composables/useGlassOpticalRenderer'
|
||||||
@@ -33,6 +34,7 @@ vi.mock('three', async importOriginal => {
|
|||||||
render() {}
|
render() {}
|
||||||
setClearColor() {}
|
setClearColor() {}
|
||||||
setPixelRatio() {}
|
setPixelRatio() {}
|
||||||
|
initTexture() {}
|
||||||
setRenderTarget() {}
|
setRenderTarget() {}
|
||||||
setScissor() {}
|
setScissor() {}
|
||||||
setScissorTest() {}
|
setScissorTest() {}
|
||||||
@@ -136,6 +138,50 @@ afterEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('glass optical surface discovery', () => {
|
describe('glass optical surface discovery', () => {
|
||||||
|
it('gives an active overlay exclusive ownership of shared pointer input', () => {
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.className = 'v-overlay v-overlay--active'
|
||||||
|
document.body.append(overlay)
|
||||||
|
const fixedListener = vi.fn()
|
||||||
|
const scrollListener = vi.fn()
|
||||||
|
const scope = effectScope()
|
||||||
|
scope.run(() => {
|
||||||
|
const source = useGlassOpticalInteractionSource()
|
||||||
|
source.subscribe('fixed', fixedListener)
|
||||||
|
source.subscribe('scroll', scrollListener)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.dispatchEvent(new MouseEvent('pointermove', { clientX: 400, clientY: 400 }))
|
||||||
|
|
||||||
|
expect(fixedListener).toHaveBeenCalledOnce()
|
||||||
|
expect(scrollListener).not.toHaveBeenCalled()
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('routes login-card input to the scroll presentation context', () => {
|
||||||
|
const loginRoot = document.createElement('div')
|
||||||
|
loginRoot.className = 'login-root'
|
||||||
|
const loginCard = document.createElement('section')
|
||||||
|
loginCard.className = 'login-card'
|
||||||
|
setOpticalSurfaceBounds(loginCard, { height: 600, width: 420, x: 200, y: 100 })
|
||||||
|
loginRoot.append(loginCard)
|
||||||
|
document.body.append(loginRoot)
|
||||||
|
const fixedListener = vi.fn()
|
||||||
|
const scrollListener = vi.fn()
|
||||||
|
const scope = effectScope()
|
||||||
|
scope.run(() => {
|
||||||
|
const source = useGlassOpticalInteractionSource()
|
||||||
|
source.subscribe('fixed', fixedListener)
|
||||||
|
source.subscribe('scroll', scrollListener)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.dispatchEvent(new MouseEvent('pointermove', { clientX: 400, clientY: 400 }))
|
||||||
|
|
||||||
|
expect(scrollListener).toHaveBeenCalledOnce()
|
||||||
|
expect(fixedListener).not.toHaveBeenCalled()
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
it('detects a target surface added directly', () => {
|
it('detects a target surface added directly', () => {
|
||||||
const surface = document.createElement('section')
|
const surface = document.createElement('section')
|
||||||
surface.dataset.glassOpticalSurface = ''
|
surface.dataset.glassOpticalSurface = ''
|
||||||
@@ -254,6 +300,20 @@ describe('glass optical surface discovery', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('discovers top-level business cards without route-specific selectors', () => {
|
||||||
|
const main = document.createElement('main')
|
||||||
|
main.className = 'layout-page-content'
|
||||||
|
const surface = document.createElement('section')
|
||||||
|
surface.className = 'v-card'
|
||||||
|
setOpticalSurfaceBounds(surface, { height: 360, width: 720, x: 120, y: 140 })
|
||||||
|
main.append(surface)
|
||||||
|
document.body.append(main)
|
||||||
|
|
||||||
|
expect(collectGlassOpticalRects(1200, 900, 'clear')).toEqual([
|
||||||
|
expect.objectContaining({ height: 360, width: 720, x: 120, y: 140 }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('recovers after consecutive WebGL context loss cycles', async () => {
|
it('recovers after consecutive WebGL context loss cycles', async () => {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const scope = effectScope()
|
const scope = effectScope()
|
||||||
@@ -321,7 +381,7 @@ describe('glass optical surface discovery', () => {
|
|||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'touchend')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'touchend')).toBe(1)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'touchcancel')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'touchcancel')).toBe(1)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'resize')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'resize')).toBe(1)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'scroll')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'scroll')).toBe(0)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'focus')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'focus')).toBe(1)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'pageshow')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'pageshow')).toBe(1)
|
||||||
expect(countListenerAdds(addDocumentListener.mock.calls, 'visibilitychange')).toBe(1)
|
expect(countListenerAdds(addDocumentListener.mock.calls, 'visibilitychange')).toBe(1)
|
||||||
@@ -353,7 +413,7 @@ describe('glass optical surface discovery', () => {
|
|||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'touchend')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'touchend')).toBe(1)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'touchcancel')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'touchcancel')).toBe(1)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'resize')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'resize')).toBe(1)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'scroll')).toBe(1)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'scroll')).toBe(0)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'focus')).toBe(0)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'focus')).toBe(0)
|
||||||
expect(countListenerAdds(addWindowListener.mock.calls, 'pageshow')).toBe(0)
|
expect(countListenerAdds(addWindowListener.mock.calls, 'pageshow')).toBe(0)
|
||||||
expect(countListenerAdds(addDocumentListener.mock.calls, 'visibilitychange')).toBe(0)
|
expect(countListenerAdds(addDocumentListener.mock.calls, 'visibilitychange')).toBe(0)
|
||||||
@@ -415,6 +475,116 @@ describe('glass optical surface discovery', () => {
|
|||||||
scope.stop()
|
scope.stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps the loaded same-origin texture while leaving the login route', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const routeKey = ref('/login')
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(canvas),
|
||||||
|
quality: ref('high'),
|
||||||
|
routeKey,
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('/api/v1/login/wallpapers/opaque-id'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
const textureLoad = vi.spyOn(three.TextureLoader.prototype, 'loadAsync')
|
||||||
|
const rendererDispose = vi.spyOn(three.WebGLRenderer.prototype, 'dispose')
|
||||||
|
|
||||||
|
routeKey.value = '/dashboard'
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(renderer?.state.value).toBe('ready')
|
||||||
|
expect(textureLoad).not.toHaveBeenCalled()
|
||||||
|
expect(rendererDispose).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears a removed fixed surface during a route transition without waiting for pointer input', async () => {
|
||||||
|
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1800)
|
||||||
|
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(900)
|
||||||
|
const three = await import('three')
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const setRenderTarget = vi.spyOn(three.WebGLRenderer.prototype, 'setRenderTarget')
|
||||||
|
const renderedRectCounts: number[] = []
|
||||||
|
render.mockImplementation(scene => {
|
||||||
|
const uniforms = (
|
||||||
|
scene as unknown as {
|
||||||
|
children: Array<{
|
||||||
|
material?: {
|
||||||
|
uniforms?: {
|
||||||
|
uRectCount?: { value: number }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
).children[0]?.material?.uniforms
|
||||||
|
if (uniforms?.uRectCount) renderedRectCounts.push(uniforms.uRectCount.value)
|
||||||
|
})
|
||||||
|
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 wrapper = document.createElement('div')
|
||||||
|
wrapper.className = 'app-wrapper'
|
||||||
|
const nav = document.createElement('aside')
|
||||||
|
nav.className = 'layout-vertical-nav'
|
||||||
|
setOpticalSurfaceBounds(nav, { height: 900, width: 260, x: 0, y: 0 })
|
||||||
|
wrapper.append(nav)
|
||||||
|
document.body.append(wrapper)
|
||||||
|
const routeKey = ref('/dashboard')
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(document.createElement('canvas')),
|
||||||
|
quality: ref('high'),
|
||||||
|
routeKey,
|
||||||
|
surfaceSpace: 'fixed',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
for (let pass = 0; pass < 5 && callbacks.size > 0; pass += 1) {
|
||||||
|
const scheduledCallbacks = [...callbacks.values()]
|
||||||
|
callbacks.clear()
|
||||||
|
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||||
|
}
|
||||||
|
render.mockClear()
|
||||||
|
setRenderTarget.mockClear()
|
||||||
|
renderedRectCounts.length = 0
|
||||||
|
|
||||||
|
nav.remove()
|
||||||
|
routeKey.value = '/login'
|
||||||
|
await nextTick()
|
||||||
|
await vi.waitFor(() => expect(callbacks.size).toBeGreaterThan(0))
|
||||||
|
for (let pass = 0; pass < 5 && callbacks.size > 0; pass += 1) {
|
||||||
|
const scheduledCallbacks = [...callbacks.values()]
|
||||||
|
callbacks.clear()
|
||||||
|
scheduledCallbacks.forEach(callback => callback(performance.now() + 100 + pass * 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(render).toHaveBeenCalled()
|
||||||
|
expect(renderedRectCounts.at(-1)).toBe(0)
|
||||||
|
expect(setRenderTarget).not.toHaveBeenCalled()
|
||||||
|
expect(callbacks.size).toBe(0)
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps old and new wallpaper textures in the same renderer during the shared transition', async () => {
|
it('keeps old and new wallpaper textures in the same renderer during the shared transition', async () => {
|
||||||
const three = await import('three')
|
const three = await import('three')
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
@@ -572,12 +742,55 @@ 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')
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
visibilityState = 'hidden'
|
visibilityState = 'hidden'
|
||||||
document.dispatchEvent(new Event('visibilitychange'))
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(APP_ACTIVITY_SUSPEND_DELAY_MS)
|
await vi.advanceTimersByTimeAsync(APP_ACTIVITY_SUSPEND_DELAY_MS)
|
||||||
expect(dispose).toHaveBeenCalledTimes(1)
|
expect(dispose).toHaveBeenCalledTimes(1)
|
||||||
|
expect(contextLoss).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
render.mockClear()
|
||||||
|
visibilityState = 'visible'
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'))
|
||||||
|
await nextTick()
|
||||||
|
await vi.waitFor(() => expect(render).toHaveBeenCalled())
|
||||||
|
expect(renderer?.state.value).toBe('ready')
|
||||||
|
expect(dispose).toHaveBeenCalledTimes(1)
|
||||||
|
expect(contextLoss).not.toHaveBeenCalled()
|
||||||
|
scope.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('releases a paused renderer after the visible window remains unfocused', async () => {
|
||||||
|
const three = await import('three')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible')
|
||||||
|
vi.spyOn(document, 'hasFocus').mockReturnValue(false)
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(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 dispose = vi.spyOn(three.WebGLRenderer.prototype, 'dispose')
|
||||||
|
const contextLoss = vi.spyOn(three.WebGLRenderer.prototype, 'forceContextLoss')
|
||||||
|
vi.useFakeTimers()
|
||||||
|
window.dispatchEvent(new Event('blur'))
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(APP_ACTIVITY_SUSPEND_DELAY_MS)
|
||||||
|
|
||||||
|
expect(dispose).toHaveBeenCalledTimes(1)
|
||||||
|
expect(contextLoss).not.toHaveBeenCalled()
|
||||||
scope.stop()
|
scope.stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -844,6 +1057,114 @@ describe('glass optical surface discovery', () => {
|
|||||||
expect(callbacks.size).toBe(0)
|
expect(callbacks.size).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('redraws a high-quality scroll layer without advancing its flow targets', async () => {
|
||||||
|
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
||||||
|
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
||||||
|
const three = await import('three')
|
||||||
|
const setRenderTarget = vi.spyOn(three.WebGLRenderer.prototype, 'setRenderTarget')
|
||||||
|
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 surface = appendOpticalSurface('app-hover-lift-card', { height: 300, width: 400, x: 40, y: 120 })
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(document.createElement('canvas')),
|
||||||
|
quality: ref('high'),
|
||||||
|
routeKey: ref('/dashboard'),
|
||||||
|
surfaceSpace: 'scroll',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
for (let pass = 0; pass < 4 && callbacks.size > 0; pass += 1) {
|
||||||
|
const scheduledCallbacks = [...callbacks.values()]
|
||||||
|
callbacks.clear()
|
||||||
|
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||||
|
}
|
||||||
|
setRenderTarget.mockClear()
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('scroll'))
|
||||||
|
for (let pass = 0; pass < 3 && callbacks.size > 0; pass += 1) {
|
||||||
|
const scheduledCallbacks = [...callbacks.values()]
|
||||||
|
callbacks.clear()
|
||||||
|
scheduledCallbacks.forEach(callback => callback(performance.now() + 100 + pass * 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(setRenderTarget).not.toHaveBeenCalled()
|
||||||
|
scope.stop()
|
||||||
|
surface.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tracks transformed card geometry during a bounded hover transition without advancing flow', async () => {
|
||||||
|
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
||||||
|
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
||||||
|
const three = await import('three')
|
||||||
|
const setRenderTarget = vi.spyOn(three.WebGLRenderer.prototype, 'setRenderTarget')
|
||||||
|
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
|
||||||
|
const callbacks = new Map<number, FrameRequestCallback>()
|
||||||
|
let frameId = 0
|
||||||
|
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
|
||||||
|
frameId += 1
|
||||||
|
callbacks.set(frameId, callback)
|
||||||
|
|
||||||
|
return frameId
|
||||||
|
})
|
||||||
|
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(id => callbacks.delete(id))
|
||||||
|
const surface = appendOpticalSurface('app-hover-lift-card', { height: 300, width: 400, x: 40, y: 120 })
|
||||||
|
const scope = effectScope()
|
||||||
|
const renderer = scope.run(() =>
|
||||||
|
useGlassOpticalRenderer({
|
||||||
|
active: ref(true),
|
||||||
|
appearance: ref('clear'),
|
||||||
|
canvas: ref(document.createElement('canvas')),
|
||||||
|
quality: ref('high'),
|
||||||
|
routeKey: ref('/plugins'),
|
||||||
|
surfaceSpace: 'scroll',
|
||||||
|
tintColor: ref('#8D51F9'),
|
||||||
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
|
||||||
|
for (let pass = 0; pass < 6 && callbacks.size > 0; pass += 1) {
|
||||||
|
const scheduledCallbacks = [...callbacks.values()]
|
||||||
|
callbacks.clear()
|
||||||
|
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
|
||||||
|
}
|
||||||
|
render.mockClear()
|
||||||
|
setRenderTarget.mockClear()
|
||||||
|
|
||||||
|
setOpticalSurfaceBounds(surface, { height: 300, width: 400, x: 40, y: 116 })
|
||||||
|
const transitionRun = new Event('transitionrun', { bubbles: true }) as TransitionEvent
|
||||||
|
Object.defineProperty(transitionRun, 'propertyName', { value: 'transform' })
|
||||||
|
surface.dispatchEvent(transitionRun)
|
||||||
|
expect(callbacks.size).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const scheduledCallbacks = [...callbacks.values()]
|
||||||
|
callbacks.clear()
|
||||||
|
scheduledCallbacks.forEach(callback => callback(performance.now() + 100))
|
||||||
|
|
||||||
|
expect(render).toHaveBeenCalled()
|
||||||
|
expect(setRenderTarget).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
const transitionEnd = new Event('transitionend', { bubbles: true }) as TransitionEvent
|
||||||
|
Object.defineProperty(transitionEnd, 'propertyName', { value: 'transform' })
|
||||||
|
surface.dispatchEvent(transitionEnd)
|
||||||
|
scope.stop()
|
||||||
|
expect(callbacks.size).toBe(0)
|
||||||
|
surface.remove()
|
||||||
|
})
|
||||||
|
|
||||||
it('refreshes visible surface slots after scrolling settles', async () => {
|
it('refreshes visible surface slots after scrolling settles', async () => {
|
||||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
|
||||||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
|
||||||
@@ -963,6 +1284,7 @@ describe('glass optical surface discovery', () => {
|
|||||||
const deformationStrength = ref(50)
|
const deformationStrength = ref(50)
|
||||||
const flowStrength = ref(50)
|
const flowStrength = ref(50)
|
||||||
const reflectionStrength = ref(50)
|
const reflectionStrength = ref(50)
|
||||||
|
const transmissionStrength = ref(50)
|
||||||
const translationStrength = ref(50)
|
const translationStrength = ref(50)
|
||||||
const scope = effectScope()
|
const scope = effectScope()
|
||||||
const renderer = scope.run(() =>
|
const renderer = scope.run(() =>
|
||||||
@@ -977,6 +1299,7 @@ describe('glass optical surface discovery', () => {
|
|||||||
routeKey: ref('/dashboard'),
|
routeKey: ref('/dashboard'),
|
||||||
surfaceSpace: 'scroll',
|
surfaceSpace: 'scroll',
|
||||||
tintColor: ref('#8D51F9'),
|
tintColor: ref('#8D51F9'),
|
||||||
|
transmissionStrength,
|
||||||
translationStrength,
|
translationStrength,
|
||||||
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
wallpaperUrl: ref('https://example.com/wallpaper.jpg'),
|
||||||
}),
|
}),
|
||||||
@@ -1005,6 +1328,7 @@ describe('glass optical surface discovery', () => {
|
|||||||
uRects: { value: Array<{ x: number }> }
|
uRects: { value: Array<{ x: number }> }
|
||||||
uReflectionStrength: { value: number }
|
uReflectionStrength: { value: number }
|
||||||
uTransparency: { value: number }
|
uTransparency: { value: number }
|
||||||
|
uTransmissionStrength: { value: number }
|
||||||
uTranslationStrength: { value: number }
|
uTranslationStrength: { value: number }
|
||||||
uSurfaceWeights: { value: number[] }
|
uSurfaceWeights: { value: number[] }
|
||||||
}
|
}
|
||||||
@@ -1034,11 +1358,13 @@ describe('glass optical surface discovery', () => {
|
|||||||
expect(uniforms.uMaxRefractionPixels.value).toBe(6)
|
expect(uniforms.uMaxRefractionPixels.value).toBe(6)
|
||||||
expect(uniforms.uReflectionStrength.value).toBe(1)
|
expect(uniforms.uReflectionStrength.value).toBe(1)
|
||||||
expect(uniforms.uTransparency.value).toBeGreaterThan(0.6)
|
expect(uniforms.uTransparency.value).toBeGreaterThan(0.6)
|
||||||
|
expect(uniforms.uTransmissionStrength.value).toBeCloseTo(5 / 7)
|
||||||
|
|
||||||
translationStrength.value = 100
|
translationStrength.value = 100
|
||||||
deformationStrength.value = 100
|
deformationStrength.value = 100
|
||||||
flowStrength.value = 100
|
flowStrength.value = 100
|
||||||
reflectionStrength.value = 80
|
reflectionStrength.value = 80
|
||||||
|
transmissionStrength.value = 100
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(uniforms.uTranslationStrength.value).toBeCloseTo(1.7)
|
expect(uniforms.uTranslationStrength.value).toBeCloseTo(1.7)
|
||||||
expect(uniforms.uDeformationStrength.value).toBeCloseTo(1.55)
|
expect(uniforms.uDeformationStrength.value).toBeCloseTo(1.55)
|
||||||
@@ -1046,6 +1372,7 @@ describe('glass optical surface discovery', () => {
|
|||||||
expect(uniforms.uMotionExpansion.value).toBe(1)
|
expect(uniforms.uMotionExpansion.value).toBe(1)
|
||||||
expect(uniforms.uMaxRefractionPixels.value).toBe(6)
|
expect(uniforms.uMaxRefractionPixels.value).toBe(6)
|
||||||
expect(uniforms.uReflectionStrength.value).toBeGreaterThan(1)
|
expect(uniforms.uReflectionStrength.value).toBeGreaterThan(1)
|
||||||
|
expect(uniforms.uTransmissionStrength.value).toBe(1.3)
|
||||||
|
|
||||||
expect(scene.children[0].material.fragmentShader).not.toContain('clamp(uMotion +')
|
expect(scene.children[0].material.fragmentShader).not.toContain('clamp(uMotion +')
|
||||||
expect(scene.children[0].material.fragmentShader).toContain(
|
expect(scene.children[0].material.fragmentShader).toContain(
|
||||||
@@ -1067,6 +1394,14 @@ describe('glass optical surface discovery', () => {
|
|||||||
expect(scene.children[0].material.fragmentShader).toContain('uniform float uFlowStrength')
|
expect(scene.children[0].material.fragmentShader).toContain('uniform float uFlowStrength')
|
||||||
expect(scene.children[0].material.fragmentShader).toContain('uniform float uReflectionStrength')
|
expect(scene.children[0].material.fragmentShader).toContain('uniform float uReflectionStrength')
|
||||||
expect(scene.children[0].material.fragmentShader).toContain('uniform float uTransparency')
|
expect(scene.children[0].material.fragmentShader).toContain('uniform float uTransparency')
|
||||||
|
expect(scene.children[0].material.fragmentShader).toContain('uniform float uTransmissionStrength')
|
||||||
|
expect(scene.children[0].material.fragmentShader).toContain('float transmissionResponse')
|
||||||
|
expect(scene.children[0].material.fragmentShader).toContain('vec3 transmissionReference')
|
||||||
|
expect(scene.children[0].material.fragmentShader).toContain('float shadowLiftGate')
|
||||||
|
expect(scene.children[0].material.fragmentShader).toContain('float shadowColorRetention')
|
||||||
|
expect(scene.children[0].material.fragmentShader).toContain('float highlightProtection')
|
||||||
|
expect(scene.children[0].material.fragmentShader).toContain('protectedHighlightReference')
|
||||||
|
expect(scene.children[0].material.fragmentShader).toContain('mix(0.58, 0.92, uQuality)')
|
||||||
expect(scene.children[0].material.fragmentShader).toContain('uMotion *')
|
expect(scene.children[0].material.fragmentShader).toContain('uMotion *')
|
||||||
expect(scene.children[0].material.fragmentShader).toContain('uTranslationStrength')
|
expect(scene.children[0].material.fragmentShader).toContain('uTranslationStrength')
|
||||||
expect(scene.children[0].material.fragmentShader).toContain('vec2 lightDirection = normalize(vec2(-0.68, 0.74))')
|
expect(scene.children[0].material.fragmentShader).toContain('vec2 lightDirection = normalize(vec2(-0.68, 0.74))')
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
expect(settings.glassFlowStrength).toBe(50)
|
expect(settings.glassFlowStrength).toBe(50)
|
||||||
expect(settings.glassPreset).toBe('natural')
|
expect(settings.glassPreset).toBe('natural')
|
||||||
expect(settings.glassQuality).toBe('balanced')
|
expect(settings.glassQuality).toBe('balanced')
|
||||||
expect(settings.glassReflectionStrength).toBe(50)
|
expect(settings.glassReflectionStrength).toBe(35)
|
||||||
|
expect(settings.glassTransmissionStrength).toBe(70)
|
||||||
expect(settings.glassTranslationStrength).toBe(50)
|
expect(settings.glassTranslationStrength).toBe(50)
|
||||||
expect(settings.glassTransparencyStrength).toBe(50)
|
expect(settings.glassTransparencyStrength).toBe(70)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each(['balanced', 'high'] as const)('preserves the %s quality contract', quality => {
|
it.each(['balanced', 'high'] as const)('preserves the %s quality contract', quality => {
|
||||||
@@ -63,6 +64,7 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
glassDeformationStrength: -12,
|
glassDeformationStrength: -12,
|
||||||
glassFlowStrength: 42.6,
|
glassFlowStrength: 42.6,
|
||||||
glassReflectionStrength: 140.6,
|
glassReflectionStrength: 140.6,
|
||||||
|
glassTransmissionStrength: 78.4,
|
||||||
glassTranslationStrength: 101,
|
glassTranslationStrength: 101,
|
||||||
glassTransparencyStrength: 83.7,
|
glassTransparencyStrength: 83.7,
|
||||||
}),
|
}),
|
||||||
@@ -72,6 +74,7 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
glassDeformationStrength: 0,
|
glassDeformationStrength: 0,
|
||||||
glassFlowStrength: 43,
|
glassFlowStrength: 43,
|
||||||
glassReflectionStrength: 100,
|
glassReflectionStrength: 100,
|
||||||
|
glassTransmissionStrength: 78,
|
||||||
glassTranslationStrength: 100,
|
glassTranslationStrength: 100,
|
||||||
glassTransparencyStrength: 84,
|
glassTransparencyStrength: 84,
|
||||||
})
|
})
|
||||||
@@ -87,6 +90,12 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps neutral transmission for stored settings created before the field existed', () => {
|
||||||
|
localStorage.setItem(THEME_CUSTOMIZER_STORAGE_KEY, JSON.stringify({ glassAppearance: 'clear' }))
|
||||||
|
|
||||||
|
expect(readThemeCustomizerSettings().glassTransmissionStrength).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
it('syncs glass settings to the document roots', () => {
|
it('syncs glass settings to the document roots', () => {
|
||||||
const settings = readThemeCustomizerSettings()
|
const settings = readThemeCustomizerSettings()
|
||||||
|
|
||||||
@@ -100,10 +109,12 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
expect(document.documentElement.dataset.glassQuality).toBe('high')
|
expect(document.documentElement.dataset.glassQuality).toBe('high')
|
||||||
expect(document.body.dataset.glassAppearance).toBe('tinted')
|
expect(document.body.dataset.glassAppearance).toBe('tinted')
|
||||||
expect(document.body.dataset.glassQuality).toBe('high')
|
expect(document.body.dataset.glassQuality).toBe('high')
|
||||||
expect(document.documentElement.style.getPropertyValue('--glass-reflection')).toBe('0.5')
|
expect(document.documentElement.style.getPropertyValue('--glass-reflection')).toBe('0.35')
|
||||||
expect(document.body.style.getPropertyValue('--glass-reflection')).toBe('0.5')
|
expect(document.body.style.getPropertyValue('--glass-reflection')).toBe('0.35')
|
||||||
expect(document.documentElement.style.getPropertyValue('--glass-transparency')).toBe('0.5')
|
expect(Number(document.documentElement.style.getPropertyValue('--glass-transmission'))).toBe(1)
|
||||||
expect(document.body.style.getPropertyValue('--glass-transparency')).toBe('0.5')
|
expect(document.body.style.getPropertyValue('--glass-transmission-brightness')).not.toBe('')
|
||||||
|
expect(Number(document.documentElement.style.getPropertyValue('--glass-transparency'))).toBeCloseTo(0.96, 2)
|
||||||
|
expect(Number(document.body.style.getPropertyValue('--glass-transparency'))).toBeCloseTo(0.96, 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('previews glass settings without persisting them', () => {
|
it('previews glass settings without persisting them', () => {
|
||||||
@@ -126,6 +137,7 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
glassPreset: 'glide',
|
glassPreset: 'glide',
|
||||||
glassQuality: 'css',
|
glassQuality: 'css',
|
||||||
glassReflectionStrength: 81,
|
glassReflectionStrength: 81,
|
||||||
|
glassTransmissionStrength: 76,
|
||||||
glassTranslationStrength: 69,
|
glassTranslationStrength: 69,
|
||||||
glassTransparencyStrength: 80,
|
glassTransparencyStrength: 80,
|
||||||
})
|
})
|
||||||
@@ -139,6 +151,7 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
glassPreset: 'glide',
|
glassPreset: 'glide',
|
||||||
glassQuality: 'css',
|
glassQuality: 'css',
|
||||||
glassReflectionStrength: 81,
|
glassReflectionStrength: 81,
|
||||||
|
glassTransmissionStrength: 76,
|
||||||
glassTranslationStrength: 69,
|
glassTranslationStrength: 69,
|
||||||
glassTransparencyStrength: 80,
|
glassTransparencyStrength: 80,
|
||||||
})
|
})
|
||||||
@@ -163,6 +176,7 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
glassDeformationStrength: 42,
|
glassDeformationStrength: 42,
|
||||||
glassFlowStrength: 44,
|
glassFlowStrength: 44,
|
||||||
glassReflectionStrength: 66,
|
glassReflectionStrength: 66,
|
||||||
|
glassTransmissionStrength: 64,
|
||||||
glassTranslationStrength: 46,
|
glassTranslationStrength: 46,
|
||||||
glassTransparencyStrength: 72,
|
glassTransparencyStrength: 72,
|
||||||
})
|
})
|
||||||
@@ -171,6 +185,7 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
glassDeformationStrength: 90,
|
glassDeformationStrength: 90,
|
||||||
glassFlowStrength: 88,
|
glassFlowStrength: 88,
|
||||||
glassReflectionStrength: 12,
|
glassReflectionStrength: 12,
|
||||||
|
glassTransmissionStrength: 92,
|
||||||
glassTranslationStrength: 86,
|
glassTranslationStrength: 86,
|
||||||
glassTransparencyStrength: 94,
|
glassTransparencyStrength: 94,
|
||||||
})
|
})
|
||||||
@@ -179,6 +194,7 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
glassDeformationStrength: 90,
|
glassDeformationStrength: 90,
|
||||||
glassFlowStrength: 88,
|
glassFlowStrength: 88,
|
||||||
glassReflectionStrength: 12,
|
glassReflectionStrength: 12,
|
||||||
|
glassTransmissionStrength: 92,
|
||||||
glassTranslationStrength: 86,
|
glassTranslationStrength: 86,
|
||||||
glassTransparencyStrength: 94,
|
glassTransparencyStrength: 94,
|
||||||
})
|
})
|
||||||
@@ -191,6 +207,7 @@ describe('useThemeCustomizer glass settings', () => {
|
|||||||
glassDeformationStrength: 42,
|
glassDeformationStrength: 42,
|
||||||
glassFlowStrength: 44,
|
glassFlowStrength: 44,
|
||||||
glassReflectionStrength: 66,
|
glassReflectionStrength: 66,
|
||||||
|
glassTransmissionStrength: 64,
|
||||||
glassTranslationStrength: 46,
|
glassTranslationStrength: 46,
|
||||||
glassTransparencyStrength: 72,
|
glassTransparencyStrength: 72,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
getGlassOpticalMotionEnergy,
|
getGlassOpticalMotionEnergy,
|
||||||
getGlassOpticalReflectionStrengthScale,
|
getGlassOpticalReflectionStrengthScale,
|
||||||
getGlassOpticalRenderProfile,
|
getGlassOpticalRenderProfile,
|
||||||
|
getGlassOpticalTransmissionStrength,
|
||||||
getGlassScrollBufferSize,
|
getGlassScrollBufferSize,
|
||||||
getGlassOpticalTransparency,
|
getGlassOpticalTransparency,
|
||||||
getGlassOpticalTranslationStrengthScale,
|
getGlassOpticalTranslationStrengthScale,
|
||||||
@@ -52,8 +53,8 @@ export type GlassRendererState = 'fallback' | 'loading' | 'ready'
|
|||||||
export type GlassPresentationSpace = 'fixed' | 'scroll'
|
export type GlassPresentationSpace = 'fixed' | 'scroll'
|
||||||
|
|
||||||
export interface GlassOpticalInteractionSource {
|
export interface GlassOpticalInteractionSource {
|
||||||
/** 订阅同一组原始指针输入;各呈现层仅负责转换自己的坐标空间。 */
|
/** 按呈现空间订阅共享输入;同一事件只会交给一个空间。 */
|
||||||
subscribe(listener: (event: PointerEvent | TouchEvent) => void): () => void
|
subscribe(space: GlassPresentationSpace, listener: (event: PointerEvent | TouchEvent) => void): () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 同步组件状态和根节点属性,确保 CSS 回退与 renderer 生命周期一致。 */
|
/** 同步组件状态和根节点属性,确保 CSS 回退与 renderer 生命周期一致。 */
|
||||||
@@ -64,9 +65,58 @@ export function setGlassRendererState(state: Ref<GlassRendererState>, value: Gla
|
|||||||
|
|
||||||
/** 为有界的多个呈现 context 建立唯一的全局指针与触摸事件源。 */
|
/** 为有界的多个呈现 context 建立唯一的全局指针与触摸事件源。 */
|
||||||
export function useGlassOpticalInteractionSource(): GlassOpticalInteractionSource {
|
export function useGlassOpticalInteractionSource(): GlassOpticalInteractionSource {
|
||||||
const listeners = new Set<(event: PointerEvent | TouchEvent) => void>()
|
const listeners: Record<GlassPresentationSpace, Set<(event: PointerEvent | TouchEvent) => void>> = {
|
||||||
|
fixed: new Set(),
|
||||||
|
scroll: new Set(),
|
||||||
|
}
|
||||||
|
const touchOwners = new Map<number, GlassPresentationSpace>()
|
||||||
|
|
||||||
|
const resolvePointOwner = (clientX: number, clientY: number): GlassPresentationSpace => {
|
||||||
|
if (document.querySelector('.v-overlay--active')) return 'fixed'
|
||||||
|
|
||||||
|
const loginCard = document.querySelector<HTMLElement>('.login-card')
|
||||||
|
if (loginCard) {
|
||||||
|
const rect = loginCard.getBoundingClientRect()
|
||||||
|
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
|
||||||
|
// 登录卡片与文档弹性滚动共用 scroll-space,输入必须落到同一呈现 context。
|
||||||
|
return document.querySelector('.login-root') ? 'scroll' : 'fixed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixedSurface = document.querySelectorAll<HTMLElement>(
|
||||||
|
'.agent-assistant-panel, .layout-navbar, .layout-vertical-nav',
|
||||||
|
)
|
||||||
|
for (const surface of fixedSurface) {
|
||||||
|
const rect = surface.getBoundingClientRect()
|
||||||
|
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) return 'fixed'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'scroll'
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveTouchOwner = (event: TouchEvent) => {
|
||||||
|
const changedTouch = event.changedTouches.item(0)
|
||||||
|
if (!changedTouch) return null
|
||||||
|
if (event.type === 'touchstart') {
|
||||||
|
const owner = resolvePointOwner(changedTouch.clientX, changedTouch.clientY)
|
||||||
|
touchOwners.set(changedTouch.identifier, owner)
|
||||||
|
return owner
|
||||||
|
}
|
||||||
|
|
||||||
|
return touchOwners.get(changedTouch.identifier) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
const dispatch = (event: PointerEvent | TouchEvent) => {
|
const dispatch = (event: PointerEvent | TouchEvent) => {
|
||||||
for (const listener of listeners) listener(event)
|
const owner =
|
||||||
|
event instanceof TouchEvent
|
||||||
|
? resolveTouchOwner(event)
|
||||||
|
: resolvePointOwner((event as PointerEvent).clientX, (event as PointerEvent).clientY)
|
||||||
|
if (!owner) return
|
||||||
|
|
||||||
|
for (const listener of listeners[owner]) listener(event)
|
||||||
|
if (event instanceof TouchEvent && (event.type === 'touchend' || event.type === 'touchcancel')) {
|
||||||
|
for (const touch of Array.from(event.changedTouches)) touchOwners.delete(touch.identifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('pointermove', dispatch, { passive: true })
|
window.addEventListener('pointermove', dispatch, { passive: true })
|
||||||
@@ -76,7 +126,9 @@ export function useGlassOpticalInteractionSource(): GlassOpticalInteractionSourc
|
|||||||
window.addEventListener('touchcancel', dispatch, { passive: true })
|
window.addEventListener('touchcancel', dispatch, { passive: true })
|
||||||
|
|
||||||
onScopeDispose(() => {
|
onScopeDispose(() => {
|
||||||
listeners.clear()
|
listeners.fixed.clear()
|
||||||
|
listeners.scroll.clear()
|
||||||
|
touchOwners.clear()
|
||||||
window.removeEventListener('pointermove', dispatch)
|
window.removeEventListener('pointermove', dispatch)
|
||||||
window.removeEventListener('touchstart', dispatch)
|
window.removeEventListener('touchstart', dispatch)
|
||||||
window.removeEventListener('touchmove', dispatch)
|
window.removeEventListener('touchmove', dispatch)
|
||||||
@@ -85,10 +137,10 @@ export function useGlassOpticalInteractionSource(): GlassOpticalInteractionSourc
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe(listener) {
|
subscribe(space, listener) {
|
||||||
listeners.add(listener)
|
listeners[space].add(listener)
|
||||||
|
|
||||||
return () => listeners.delete(listener)
|
return () => listeners[space].delete(listener)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +171,7 @@ interface GlassRendererUniforms extends Record<string, IUniform> {
|
|||||||
uTextureMix: IUniform<number>
|
uTextureMix: IUniform<number>
|
||||||
uTintColor: IUniform<Color>
|
uTintColor: IUniform<Color>
|
||||||
uTransparency: IUniform<number>
|
uTransparency: IUniform<number>
|
||||||
|
uTransmissionStrength: IUniform<number>
|
||||||
uTranslationStrength: IUniform<number>
|
uTranslationStrength: IUniform<number>
|
||||||
uTrail: IUniform<Vector4[]>
|
uTrail: IUniform<Vector4[]>
|
||||||
uTrailCount: IUniform<number>
|
uTrailCount: IUniform<number>
|
||||||
@@ -169,6 +222,8 @@ interface UseGlassOpticalRendererOptions {
|
|||||||
quality: MaybeRefOrGetter<GlassOpticalQuality>
|
quality: MaybeRefOrGetter<GlassOpticalQuality>
|
||||||
reflectionStrength?: MaybeRefOrGetter<number>
|
reflectionStrength?: MaybeRefOrGetter<number>
|
||||||
previousWallpaperUrl?: MaybeRefOrGetter<string>
|
previousWallpaperUrl?: MaybeRefOrGetter<string>
|
||||||
|
/** 下一张壁纸可在提交切换前完成解码和 GPU 上传。 */
|
||||||
|
pendingWallpaperUrl?: MaybeRefOrGetter<string>
|
||||||
transparencyStrength?: MaybeRefOrGetter<number>
|
transparencyStrength?: MaybeRefOrGetter<number>
|
||||||
routeKey: MaybeRefOrGetter<string>
|
routeKey: MaybeRefOrGetter<string>
|
||||||
syncDocumentState?: boolean
|
syncDocumentState?: boolean
|
||||||
@@ -176,12 +231,25 @@ interface UseGlassOpticalRendererOptions {
|
|||||||
tintColor: MaybeRefOrGetter<string>
|
tintColor: MaybeRefOrGetter<string>
|
||||||
transitionDuration?: MaybeRefOrGetter<number>
|
transitionDuration?: MaybeRefOrGetter<number>
|
||||||
transitionStartedAt?: MaybeRefOrGetter<number>
|
transitionStartedAt?: MaybeRefOrGetter<number>
|
||||||
|
/** 玻璃内部壁纸采样的透射亮度。 */
|
||||||
|
transmissionStrength?: MaybeRefOrGetter<number>
|
||||||
translationStrength?: MaybeRefOrGetter<number>
|
translationStrength?: MaybeRefOrGetter<number>
|
||||||
wallpaperUrl: MaybeRefOrGetter<string>
|
wallpaperUrl: MaybeRefOrGetter<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
type GlassOpticalSurfaceDescriptor = GlassOpticalSurfaceCandidate<HTMLElement>
|
type GlassOpticalSurfaceDescriptor = GlassOpticalSurfaceCandidate<HTMLElement>
|
||||||
|
|
||||||
|
interface PreparedWallpaperTexture {
|
||||||
|
/** 是否包含可采样的真实壁纸,而非程序化回退纹理。 */
|
||||||
|
hasWallpaperTexture: boolean
|
||||||
|
/** 纹理像素高度。 */
|
||||||
|
height: number
|
||||||
|
/** 已完成当前 WebGL context 上传的纹理。 */
|
||||||
|
texture: Texture
|
||||||
|
/** 纹理像素宽度。 */
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
|
||||||
const SURFACE_SELECTORS = [
|
const SURFACE_SELECTORS = [
|
||||||
{ rank: 1, selector: '.v-overlay--active .v-overlay__content > .v-card', space: 'fixed' },
|
{ rank: 1, selector: '.v-overlay--active .v-overlay__content > .v-card', space: 'fixed' },
|
||||||
{ rank: 1, selector: '.v-overlay--active .v-overlay__content > .v-sheet', space: 'fixed' },
|
{ rank: 1, selector: '.v-overlay--active .v-overlay__content > .v-sheet', space: 'fixed' },
|
||||||
@@ -203,9 +271,19 @@ const SURFACE_SELECTORS = [
|
|||||||
{ rank: 3, selector: '[data-glass-optical-surface]', space: 'scroll' },
|
{ rank: 3, selector: '[data-glass-optical-surface]', space: 'scroll' },
|
||||||
// 推荐、订阅、媒体详情与设置页共用该交互卡片契约,不按业务路由维护 renderer 白名单。
|
// 推荐、订阅、媒体详情与设置页共用该交互卡片契约,不按业务路由维护 renderer 白名单。
|
||||||
{ rank: 4, selector: '.app-hover-lift-card', space: 'scroll' },
|
{ rank: 4, selector: '.app-hover-lift-card', space: 'scroll' },
|
||||||
|
// 顶层业务卡片共享玻璃表面语义;嵌套卡片由表面收集阶段折叠,避免按页面维护白名单。
|
||||||
|
{ rank: 5, selector: '.layout-page-content .v-card', space: 'scroll' },
|
||||||
] as const
|
] as const
|
||||||
const SURFACE_SELECTOR_QUERY = SURFACE_SELECTORS.map(({ selector }) => selector).join(',')
|
const SURFACE_SELECTOR_QUERY = SURFACE_SELECTORS.map(({ selector }) => selector).join(',')
|
||||||
|
|
||||||
|
/** 登录卡片随文档弹性合成,其余固定表面继续使用 viewport 坐标。 */
|
||||||
|
function getSurfacePresentationSpace(
|
||||||
|
selector: (typeof SURFACE_SELECTORS)[number]['selector'],
|
||||||
|
defaultSpace: GlassPresentationSpace,
|
||||||
|
) {
|
||||||
|
return selector === '.login-card' && document.querySelector('.login-root') ? 'scroll' : defaultSpace
|
||||||
|
}
|
||||||
|
|
||||||
/** 判断新增或移除的 DOM 子树是否会改变光学表面集合。 */
|
/** 判断新增或移除的 DOM 子树是否会改变光学表面集合。 */
|
||||||
export function containsGlassOpticalSurface(node: Node) {
|
export function containsGlassOpticalSurface(node: Node) {
|
||||||
return (
|
return (
|
||||||
@@ -293,6 +371,7 @@ uniform int uRectCount;
|
|||||||
uniform float uAppearance;
|
uniform float uAppearance;
|
||||||
uniform vec3 uTintColor;
|
uniform vec3 uTintColor;
|
||||||
uniform float uTransparency;
|
uniform float uTransparency;
|
||||||
|
uniform float uTransmissionStrength;
|
||||||
uniform float uTranslationStrength;
|
uniform float uTranslationStrength;
|
||||||
uniform float uTextureMix;
|
uniform float uTextureMix;
|
||||||
uniform vec4 uTrail[4];
|
uniform vec4 uTrail[4];
|
||||||
@@ -338,14 +417,49 @@ vec3 toneMapWallpaper(vec3 color, vec2 uv) {
|
|||||||
mapped = clamp((mapped - vec3(0.5)) * contrast + vec3(0.5), 0.0, 1.0) * exposure;
|
mapped = clamp((mapped - vec3(0.5)) * contrast + vec3(0.5), 0.0, 1.0) * exposure;
|
||||||
|
|
||||||
float top = 1.0 - uv.y;
|
float top = 1.0 - uv.y;
|
||||||
float linearStart = mix(0.14, 0.3, frosted);
|
float linearStart = mix(0.1, 0.24, frosted);
|
||||||
float linearEnd = mix(mix(0.4, 0.42, tinted), 0.58, frosted);
|
float linearEnd = mix(mix(0.3, 0.32, tinted), 0.48, frosted);
|
||||||
float linearAbsorption = mix(linearStart, linearEnd, top);
|
float linearAbsorption = mix(linearStart, linearEnd, top);
|
||||||
vec2 radialDelta = (uv - vec2(0.5, 0.82)) / vec2(0.78, 1.0);
|
vec2 radialDelta = (uv - vec2(0.5, 0.82)) / vec2(0.78, 1.0);
|
||||||
float radialAbsorption = smoothstep(0.24, 0.92, length(radialDelta)) * mix(0.16, 0.18, tinted);
|
float radialAbsorption = smoothstep(0.24, 0.92, length(radialDelta)) * mix(0.12, 0.14, tinted);
|
||||||
radialAbsorption *= 1.0 - frosted;
|
radialAbsorption *= 1.0 - frosted;
|
||||||
|
vec3 absorbed = mapped * (1.0 - linearAbsorption) * (1.0 - radialAbsorption);
|
||||||
|
float transmissionExpansion = max(0.0, (uTransmissionStrength - 0.5) * 2.0);
|
||||||
|
float transmissionResponse = pow(transmissionExpansion, mix(0.8, 0.65, uQuality));
|
||||||
|
float transmissionMaterialScale = mix(1.0, 0.78, tinted);
|
||||||
|
transmissionMaterialScale = mix(transmissionMaterialScale, 0.42, frosted);
|
||||||
|
float shadowGamma =
|
||||||
|
1.0 -
|
||||||
|
transmissionResponse *
|
||||||
|
transmissionMaterialScale *
|
||||||
|
mix(0.28, 0.5, uQuality);
|
||||||
|
float shadowLiftGate = smoothstep(0.035, 0.2, luminance);
|
||||||
|
float highlightProtection = smoothstep(0.72, 0.96, luminance);
|
||||||
|
float protectedGamma = mix(max(shadowGamma, 0.72), shadowGamma, shadowLiftGate);
|
||||||
|
protectedGamma = mix(protectedGamma, 1.0, highlightProtection);
|
||||||
|
vec3 expandedSource = pow(max(color, vec3(0.0)), vec3(protectedGamma));
|
||||||
|
float sourceLuminance = dot(expandedSource, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
float shadowColorRetention = mix(0.42, 1.0, smoothstep(0.025, 0.18, luminance));
|
||||||
|
expandedSource = mix(vec3(sourceLuminance), expandedSource, shadowColorRetention);
|
||||||
|
sourceLuminance = dot(expandedSource, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
vec3 transmissionReference = mix(vec3(sourceLuminance), expandedSource, mix(1.16, 1.08, frosted));
|
||||||
|
transmissionReference =
|
||||||
|
clamp((transmissionReference - vec3(0.5)) * 1.06 + vec3(0.5), 0.0, 1.0) *
|
||||||
|
1.04;
|
||||||
|
vec3 protectedHighlightReference = min(color * 1.02, vec3(1.0));
|
||||||
|
transmissionReference = mix(
|
||||||
|
transmissionReference,
|
||||||
|
protectedHighlightReference,
|
||||||
|
highlightProtection * 0.72
|
||||||
|
);
|
||||||
|
float transmissionMix = min(
|
||||||
|
transmissionResponse *
|
||||||
|
transmissionMaterialScale *
|
||||||
|
mix(0.58, 0.92, uQuality),
|
||||||
|
0.94
|
||||||
|
);
|
||||||
|
|
||||||
return mapped * (1.0 - linearAbsorption) * (1.0 - radialAbsorption);
|
return mix(absorbed, min(transmissionReference, vec3(1.0)), transmissionMix);
|
||||||
}
|
}
|
||||||
|
|
||||||
vec3 sampleWallpaper(vec2 uv) {
|
vec3 sampleWallpaper(vec2 uv) {
|
||||||
@@ -603,6 +717,13 @@ void main() {
|
|||||||
}
|
}
|
||||||
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 transmissionOffset = (uTransmissionStrength - 0.5) * 2.0;
|
||||||
|
if (transmissionOffset < 0.0) {
|
||||||
|
float dimming = mix(0.18, 0.22, uQuality) * mix(1.0, 0.65, frosted);
|
||||||
|
refracted *= 1.0 + transmissionOffset * dimming;
|
||||||
|
}
|
||||||
|
refractedLuminance = dot(refracted, vec3(0.2126, 0.7152, 0.0722));
|
||||||
float highlightBudget = mix(1.0, 0.34, smoothstep(0.48, 0.9, refractedLuminance));
|
float highlightBudget = mix(1.0, 0.34, smoothstep(0.48, 0.9, refractedLuminance));
|
||||||
float frostedBrightCompression = smoothstep(0.58, 0.94, refractedLuminance) * frosted;
|
float frostedBrightCompression = smoothstep(0.58, 0.94, refractedLuminance) * frosted;
|
||||||
refracted *= 1.0 - frostedBrightCompression * mix(0.16, 0.22, uQuality);
|
refracted *= 1.0 - frostedBrightCompression * mix(0.16, 0.22, uQuality);
|
||||||
@@ -686,10 +807,12 @@ void main() {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const SCROLL_SURFACE_UPDATE_INTERVAL_MS = 32
|
|
||||||
const SCROLL_STABLE_TAIL_FRAMES = 2
|
const SCROLL_STABLE_TAIL_FRAMES = 2
|
||||||
|
const SURFACE_STABILITY_MAX_FRAMES = 6
|
||||||
|
const SURFACE_STABILITY_REQUIRED_FRAMES = 2
|
||||||
const FLOW_BUFFER_SCALE = 0.25
|
const FLOW_BUFFER_SCALE = 0.25
|
||||||
const SURFACE_TRANSITION_DURATION_MS = 96
|
const SURFACE_TRANSITION_DURATION_MS = 96
|
||||||
|
const SURFACE_TRANSFORM_TRACKING_MAX_MS = 1000
|
||||||
|
|
||||||
/** 按 shader 协议读取视觉表面的四角圆角。 */
|
/** 按 shader 协议读取视觉表面的四角圆角。 */
|
||||||
function readBorderRadii(element: HTMLElement) {
|
function readBorderRadii(element: HTMLElement) {
|
||||||
@@ -732,7 +855,8 @@ function collectGlassOpticalSurfaceDescriptors(
|
|||||||
const seen = new Set<HTMLElement>()
|
const seen = new Set<HTMLElement>()
|
||||||
|
|
||||||
for (const { rank, selector, space } of SURFACE_SELECTORS) {
|
for (const { rank, selector, space } of SURFACE_SELECTORS) {
|
||||||
if (surfaceSpace !== 'all' && space !== surfaceSpace) continue
|
const resolvedSpace = getSurfacePresentationSpace(selector, space)
|
||||||
|
if (surfaceSpace !== 'all' && resolvedSpace !== surfaceSpace) continue
|
||||||
// 移动端透明顶栏只使用稳定的 CSS 表面,避免滚动重扫时再次叠加壁纸折射。
|
// 移动端透明顶栏只使用稳定的 CSS 表面,避免滚动重扫时再次叠加壁纸折射。
|
||||||
if (viewportWidth <= 600 && appearance === 'clear' && selector === '.layout-navbar') continue
|
if (viewportWidth <= 600 && appearance === 'clear' && selector === '.layout-navbar') continue
|
||||||
|
|
||||||
@@ -750,8 +874,8 @@ function collectGlassOpticalSurfaceDescriptors(
|
|||||||
const visibleHeight = Math.max(0, bottom - top)
|
const visibleHeight = Math.max(0, bottom - top)
|
||||||
const visibleWidth = Math.max(0, right - left)
|
const visibleWidth = Math.max(0, right - left)
|
||||||
if (visibleWidth < 24 || visibleHeight < 24) continue
|
if (visibleWidth < 24 || visibleHeight < 24) continue
|
||||||
const coordinateOffsetX = surfaceSpace === 'scroll' ? window.scrollX : 0
|
const coordinateOffsetX = resolvedSpace === 'scroll' ? window.scrollX : 0
|
||||||
const coordinateOffsetY = surfaceSpace === 'scroll' ? window.scrollY : 0
|
const coordinateOffsetY = resolvedSpace === 'scroll' ? window.scrollY : 0
|
||||||
|
|
||||||
candidates.push({
|
candidates.push({
|
||||||
key: element,
|
key: element,
|
||||||
@@ -817,6 +941,7 @@ function getGlassAppearanceUniformValue(appearance: ThemeCustomizerGlassAppearan
|
|||||||
export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions) {
|
export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions) {
|
||||||
const state = ref<GlassRendererState>('loading')
|
const state = ref<GlassRendererState>('loading')
|
||||||
const renderedFrames = ref(0)
|
const renderedFrames = ref(0)
|
||||||
|
const preparedWallpaperUrl = ref('')
|
||||||
let three: ThreeModule | null = null
|
let three: ThreeModule | null = null
|
||||||
let resources: GlassRendererResources | null = null
|
let resources: GlassRendererResources | null = null
|
||||||
let flowResources: GlassFlowResources | null = null
|
let flowResources: GlassFlowResources | null = null
|
||||||
@@ -827,12 +952,18 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
let previousTextureHeight = 1
|
let previousTextureHeight = 1
|
||||||
let previousTextureWidth = 1
|
let previousTextureWidth = 1
|
||||||
let loadVersion = 0
|
let loadVersion = 0
|
||||||
|
let prepareVersion = 0
|
||||||
|
let preparedWallpaper: PreparedWallpaperTexture | null = null
|
||||||
let animationFrame: number | null = null
|
let animationFrame: number | null = null
|
||||||
let wallpaperTransitionFrame: number | null = null
|
let wallpaperTransitionFrame: number | null = null
|
||||||
let backgroundDisposeTimer: number | null = null
|
let backgroundDisposeTimer: number | null = null
|
||||||
let presentationBufferHeight = 1
|
let presentationBufferHeight = 1
|
||||||
let presentationBufferWidth = 1
|
let presentationBufferWidth = 1
|
||||||
let surfaceUpdateFrame: number | null = null
|
let surfaceUpdateFrame: number | null = null
|
||||||
|
let surfaceStabilityFrame: number | null = null
|
||||||
|
let surfaceStabilityPass = 0
|
||||||
|
let surfaceStableFrameCount = 0
|
||||||
|
let lastSurfaceGeometrySignature = ''
|
||||||
let lastInteractionAt = 0
|
let lastInteractionAt = 0
|
||||||
let lastInteractionFrameAt = 0
|
let lastInteractionFrameAt = 0
|
||||||
let lastPointerAt = 0
|
let lastPointerAt = 0
|
||||||
@@ -855,9 +986,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
let scrollStableFrameCount = 0
|
let scrollStableFrameCount = 0
|
||||||
let lastRenderedScrollX = window.scrollX
|
let lastRenderedScrollX = window.scrollX
|
||||||
let lastRenderedScrollY = window.scrollY
|
let lastRenderedScrollY = window.scrollY
|
||||||
let scrollSurfaceTimer: number | null = null
|
|
||||||
let unsubscribeInteractionSource: (() => void) | null = null
|
let unsubscribeInteractionSource: (() => void) | null = null
|
||||||
let lastScrollSurfaceUpdateAt = 0
|
|
||||||
let resizeObserver: ResizeObserver | null = null
|
let resizeObserver: ResizeObserver | null = null
|
||||||
let surfaceMutationObserver: MutationObserver | null = null
|
let surfaceMutationObserver: MutationObserver | null = null
|
||||||
let observedSurfaces: HTMLElement[] = []
|
let observedSurfaces: HTMLElement[] = []
|
||||||
@@ -866,8 +995,10 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
let activeSurface: HTMLElement | null = null
|
let activeSurface: HTMLElement | null = null
|
||||||
let outgoingSurface: HTMLElement | null = null
|
let outgoingSurface: HTMLElement | null = null
|
||||||
let surfaceTransitionStartedAt = 0
|
let surfaceTransitionStartedAt = 0
|
||||||
|
let surfaceTransformFrame: number | null = null
|
||||||
|
let surfaceTransformTrackingDeadline = 0
|
||||||
|
const transformingSurfaces = new Set<HTMLElement>()
|
||||||
let wakeDirection = { x: 0, y: -1 }
|
let wakeDirection = { x: 0, y: -1 }
|
||||||
let tracksScrollingSurfaces = false
|
|
||||||
let contextRecoveryPending = false
|
let contextRecoveryPending = false
|
||||||
const presentationSpace = options.surfaceSpace ?? 'fixed'
|
const presentationSpace = options.surfaceSpace ?? 'fixed'
|
||||||
|
|
||||||
@@ -921,6 +1052,13 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
wallpaperTransitionFrame = null
|
wallpaperTransitionFrame = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelSurfaceTransformFrame() {
|
||||||
|
if (surfaceTransformFrame !== null) cancelAnimationFrame(surfaceTransformFrame)
|
||||||
|
surfaceTransformFrame = null
|
||||||
|
surfaceTransformTrackingDeadline = 0
|
||||||
|
transformingSurfaces.clear()
|
||||||
|
}
|
||||||
|
|
||||||
function clearBackgroundDisposeTimer() {
|
function clearBackgroundDisposeTimer() {
|
||||||
if (backgroundDisposeTimer === null) return
|
if (backgroundDisposeTimer === null) return
|
||||||
|
|
||||||
@@ -1037,11 +1175,11 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
resources.uniforms.uHasFlowTexture.value = 1
|
resources.uniforms.uHasFlowTexture.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFrame(timestamp = performance.now()) {
|
function renderFrame(timestamp = performance.now(), advanceFlow = true) {
|
||||||
if (!resources || !toValue(options.active) || document.visibilityState === 'hidden') return
|
if (!resources || !toValue(options.active) || document.visibilityState === 'hidden') return
|
||||||
|
|
||||||
updateWallpaperTransition(timestamp)
|
updateWallpaperTransition(timestamp)
|
||||||
if (flowResources) {
|
if (flowResources && advanceFlow) {
|
||||||
resources.renderer.setScissorTest(false)
|
resources.renderer.setScissorTest(false)
|
||||||
flowResources.uniforms.uPrevious.value = flowResources.readTarget.texture
|
flowResources.uniforms.uPrevious.value = flowResources.readTarget.texture
|
||||||
resources.renderer.setRenderTarget(flowResources.writeTarget)
|
resources.renderer.setRenderTarget(flowResources.writeTarget)
|
||||||
@@ -1148,9 +1286,9 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
const nextObservedSurfaces = Array.from(
|
const nextObservedSurfaces = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
SURFACE_SELECTORS.filter(({ space }) => space === presentationSpace).flatMap(({ selector }) =>
|
SURFACE_SELECTORS.filter(
|
||||||
Array.from(document.querySelectorAll<HTMLElement>(selector)),
|
({ selector, space }) => getSurfacePresentationSpace(selector, space) === presentationSpace,
|
||||||
),
|
).flatMap(({ selector }) => Array.from(document.querySelectorAll<HTMLElement>(selector))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const observedSurfacesChanged =
|
const observedSurfacesChanged =
|
||||||
@@ -1162,19 +1300,55 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
observedSurfaces = nextObservedSurfaces
|
observedSurfaces = nextObservedSurfaces
|
||||||
for (const element of observedSurfaces) resizeObserver?.observe(element)
|
for (const element of observedSurfaces) resizeObserver?.observe(element)
|
||||||
}
|
}
|
||||||
tracksScrollingSurfaces = observedSurfaces.length > 0
|
|
||||||
if (scheduleRender) scheduleFrame()
|
if (scheduleRender) scheduleFrame()
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleSurfaceUpdate() {
|
function scheduleSurfaceUpdate() {
|
||||||
if (surfaceUpdateFrame !== null || !resources) return
|
if (surfaceUpdateFrame !== null || !resources) return
|
||||||
|
|
||||||
surfaceUpdateFrame = requestAnimationFrame(() => {
|
surfaceUpdateFrame = requestAnimationFrame(timestamp => {
|
||||||
surfaceUpdateFrame = null
|
surfaceUpdateFrame = null
|
||||||
updateSurfaceUniforms()
|
updateSurfaceUniforms(timestamp, false)
|
||||||
|
// 表面失效必须在同一有界帧内清除旧像素,不能等待下一次指针或壁纸事件。
|
||||||
|
renderFrame(timestamp, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** DOM 重排后连续采样少量帧,避免把虚拟列表的中间几何误认为最终表面。 */
|
||||||
|
function scheduleSurfaceStabilityUpdate() {
|
||||||
|
surfaceStabilityPass = 0
|
||||||
|
surfaceStableFrameCount = 0
|
||||||
|
lastSurfaceGeometrySignature = ''
|
||||||
|
if (surfaceStabilityFrame !== null || !resources) return
|
||||||
|
|
||||||
|
const sample = (timestamp: number) => {
|
||||||
|
surfaceStabilityFrame = null
|
||||||
|
if (!resources || !toValue(options.active) || document.visibilityState === 'hidden') return
|
||||||
|
|
||||||
|
updateSurfaceUniforms(timestamp, false)
|
||||||
|
const signature = surfaceSlots
|
||||||
|
.map(slot => {
|
||||||
|
const { height, width, x, y } = slot.rect
|
||||||
|
return `${x.toFixed(2)},${y.toFixed(2)},${width.toFixed(2)},${height.toFixed(2)}`
|
||||||
|
})
|
||||||
|
.join('|')
|
||||||
|
surfaceStableFrameCount = signature === lastSurfaceGeometrySignature ? surfaceStableFrameCount + 1 : 0
|
||||||
|
lastSurfaceGeometrySignature = signature
|
||||||
|
surfaceStabilityPass += 1
|
||||||
|
// DOM 删除和虚拟列表重排只刷新合成几何,不推进已有液态流场。
|
||||||
|
renderFrame(timestamp, false)
|
||||||
|
|
||||||
|
if (
|
||||||
|
surfaceStableFrameCount < SURFACE_STABILITY_REQUIRED_FRAMES &&
|
||||||
|
surfaceStabilityPass < SURFACE_STABILITY_MAX_FRAMES
|
||||||
|
) {
|
||||||
|
surfaceStabilityFrame = requestAnimationFrame(sample)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
surfaceStabilityFrame = requestAnimationFrame(sample)
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleSurfaceResizeUpdate() {
|
function scheduleSurfaceResizeUpdate() {
|
||||||
if (surfaceResizeTimer !== null) window.clearTimeout(surfaceResizeTimer)
|
if (surfaceResizeTimer !== null) window.clearTimeout(surfaceResizeTimer)
|
||||||
|
|
||||||
@@ -1185,6 +1359,65 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}, 160)
|
}, 160)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** CSS transform 不改变布局尺寸,过渡期间用有界帧同步真实几何并清除旧蒙版。 */
|
||||||
|
function scheduleSurfaceTransformFrame() {
|
||||||
|
if (surfaceTransformFrame !== null || !resources) return
|
||||||
|
|
||||||
|
surfaceTransformFrame = requestAnimationFrame(timestamp => {
|
||||||
|
surfaceTransformFrame = null
|
||||||
|
if (!resources || !toValue(options.active) || document.visibilityState === 'hidden') {
|
||||||
|
cancelSurfaceTransformFrame()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSurfaceUniforms(timestamp, false)
|
||||||
|
renderFrame(timestamp, false)
|
||||||
|
if (transformingSurfaces.size > 0 && timestamp < surfaceTransformTrackingDeadline) {
|
||||||
|
scheduleSurfaceTransformFrame()
|
||||||
|
} else {
|
||||||
|
transformingSurfaces.clear()
|
||||||
|
scheduleSurfaceStabilityUpdate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTransitionSurface(target: EventTarget | null) {
|
||||||
|
if (!(target instanceof HTMLElement)) return null
|
||||||
|
|
||||||
|
const surface = target.matches(SURFACE_SELECTOR_QUERY)
|
||||||
|
? target
|
||||||
|
: target.closest<HTMLElement>(SURFACE_SELECTOR_QUERY)
|
||||||
|
if (!surface) return null
|
||||||
|
|
||||||
|
return SURFACE_SELECTORS.some(
|
||||||
|
({ selector, space }) =>
|
||||||
|
surface.matches(selector) && getSurfacePresentationSpace(selector, space) === presentationSpace,
|
||||||
|
)
|
||||||
|
? surface
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSurfaceTransitionRun(event: TransitionEvent) {
|
||||||
|
if (event.propertyName !== 'transform') return
|
||||||
|
|
||||||
|
const surface = resolveTransitionSurface(event.target)
|
||||||
|
if (!surface) return
|
||||||
|
|
||||||
|
transformingSurfaces.add(surface)
|
||||||
|
surfaceTransformTrackingDeadline = performance.now() + SURFACE_TRANSFORM_TRACKING_MAX_MS
|
||||||
|
scheduleSurfaceTransformFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSurfaceTransitionEnd(event: TransitionEvent) {
|
||||||
|
if (event.propertyName !== 'transform') return
|
||||||
|
|
||||||
|
const surface = resolveTransitionSurface(event.target)
|
||||||
|
if (!surface) return
|
||||||
|
|
||||||
|
transformingSurfaces.delete(surface)
|
||||||
|
if (transformingSurfaces.size === 0) scheduleSurfaceStabilityUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
function syncCoverScale(viewportWidth = window.innerWidth, viewportHeight = window.innerHeight) {
|
function syncCoverScale(viewportWidth = window.innerWidth, viewportHeight = window.innerHeight) {
|
||||||
if (!resources) return
|
if (!resources) return
|
||||||
|
|
||||||
@@ -1280,9 +1513,17 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
previousProfile: ReturnType<typeof getGlassOpticalRenderProfile>,
|
previousProfile: ReturnType<typeof getGlassOpticalRenderProfile>,
|
||||||
nextProfile: ReturnType<typeof getGlassOpticalRenderProfile>,
|
nextProfile: ReturnType<typeof getGlassOpticalRenderProfile>,
|
||||||
) {
|
) {
|
||||||
|
const wallpaperUrl = toValue(options.wallpaperUrl)
|
||||||
|
const resolveTextureSource = (profile: ReturnType<typeof getGlassOpticalRenderProfile>) =>
|
||||||
|
profile.textureSource === 'auto'
|
||||||
|
? canUseGlassWallpaperTexture(wallpaperUrl, window.location.href)
|
||||||
|
? 'wallpaper'
|
||||||
|
: 'procedural'
|
||||||
|
: profile.textureSource
|
||||||
|
|
||||||
return (
|
return (
|
||||||
previousProfile.textureLimit !== nextProfile.textureLimit ||
|
previousProfile.textureLimit !== nextProfile.textureLimit ||
|
||||||
previousProfile.textureSource !== nextProfile.textureSource
|
resolveTextureSource(previousProfile) !== resolveTextureSource(nextProfile)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1661,7 +1902,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
scrollSurfaceRefreshPending = false
|
scrollSurfaceRefreshPending = false
|
||||||
updateSurfaceUniforms(timestamp, false)
|
updateSurfaceUniforms(timestamp, false)
|
||||||
}
|
}
|
||||||
if (!interactionAnimating) renderFrame(timestamp)
|
if (!interactionAnimating) renderFrame(timestamp, false)
|
||||||
|
|
||||||
if (scrollStableFrameCount < SCROLL_STABLE_TAIL_FRAMES) {
|
if (scrollStableFrameCount < SCROLL_STABLE_TAIL_FRAMES) {
|
||||||
scrollAnimationFrame = requestAnimationFrame(renderScrollFrame)
|
scrollAnimationFrame = requestAnimationFrame(renderScrollFrame)
|
||||||
@@ -1682,17 +1923,6 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
scheduleScrollFrame()
|
scheduleScrollFrame()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = performance.now()
|
|
||||||
if (!tracksScrollingSurfaces || scrollSurfaceTimer !== null) return
|
|
||||||
|
|
||||||
const elapsed = timestamp - lastScrollSurfaceUpdateAt
|
|
||||||
const delay = Math.max(0, SCROLL_SURFACE_UPDATE_INTERVAL_MS - elapsed)
|
|
||||||
scrollSurfaceTimer = window.setTimeout(() => {
|
|
||||||
scrollSurfaceTimer = null
|
|
||||||
lastScrollSurfaceUpdateAt = performance.now()
|
|
||||||
scheduleSurfaceUpdate()
|
|
||||||
}, delay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScrollEnd() {
|
function handleScrollEnd() {
|
||||||
@@ -1709,6 +1939,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
cancelScheduledFrame()
|
cancelScheduledFrame()
|
||||||
cancelScrollFrame()
|
cancelScrollFrame()
|
||||||
cancelWallpaperTransitionFrame()
|
cancelWallpaperTransitionFrame()
|
||||||
|
cancelSurfaceTransformFrame()
|
||||||
interactionAnimating = false
|
interactionAnimating = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1730,16 +1961,22 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
scheduleWallpaperTransition()
|
scheduleWallpaperTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleVisibilityChange() {
|
/** 持续非活动才释放 GPU 资源,短时切换应用继续保留纹理与稳定画面。 */
|
||||||
if (document.visibilityState === 'hidden') {
|
function scheduleInactiveRendererDisposal(isStillInactive: () => boolean) {
|
||||||
pauseRenderer()
|
|
||||||
clearBackgroundDisposeTimer()
|
clearBackgroundDisposeTimer()
|
||||||
backgroundDisposeTimer = window.setTimeout(() => {
|
backgroundDisposeTimer = window.setTimeout(() => {
|
||||||
backgroundDisposeTimer = null
|
backgroundDisposeTimer = null
|
||||||
if (document.visibilityState !== 'hidden') return
|
if (!isStillInactive()) return
|
||||||
|
|
||||||
disposeRenderer()
|
// 生命周期暂停后仍会复用同一 canvas;只释放 renderer 资源,不主动丢失其 WebGL context。
|
||||||
|
disposeRenderer(false)
|
||||||
}, APP_ACTIVITY_SUSPEND_DELAY_MS)
|
}, APP_ACTIVITY_SUSPEND_DELAY_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVisibilityChange() {
|
||||||
|
if (document.visibilityState === 'hidden') {
|
||||||
|
pauseRenderer()
|
||||||
|
scheduleInactiveRendererDisposal(() => document.visibilityState === 'hidden')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1747,7 +1984,10 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowBlur() {
|
function handleWindowBlur() {
|
||||||
if (document.visibilityState === 'visible') pauseRenderer()
|
if (document.visibilityState !== 'visible') return
|
||||||
|
|
||||||
|
pauseRenderer()
|
||||||
|
scheduleInactiveRendererDisposal(() => document.visibilityState === 'visible' && !document.hasFocus())
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowResume() {
|
function handleWindowResume() {
|
||||||
@@ -1791,7 +2031,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
surfaceMutationObserver = new MutationObserver(mutations => {
|
surfaceMutationObserver = new MutationObserver(mutations => {
|
||||||
// Vuetify 可能在首个弹层打开时才创建容器,后续变更需要纳入同一个表面生命周期。
|
// Vuetify 可能在首个弹层打开时才创建容器,后续变更需要纳入同一个表面生命周期。
|
||||||
observeMutationRoot(document.querySelector('.v-overlay-container'), true)
|
observeMutationRoot(document.querySelector('.v-overlay-container'), true)
|
||||||
if (mutationTouchesOpticalSurface(mutations)) scheduleSurfaceUpdate()
|
if (mutationTouchesOpticalSurface(mutations)) scheduleSurfaceStabilityUpdate()
|
||||||
})
|
})
|
||||||
observeMutationRoot(document.querySelector('.app-wrapper'), true)
|
observeMutationRoot(document.querySelector('.app-wrapper'), true)
|
||||||
observeMutationRoot(document.querySelector('.v-overlay-container'), true)
|
observeMutationRoot(document.querySelector('.v-overlay-container'), true)
|
||||||
@@ -1804,7 +2044,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
function setupEvents() {
|
function setupEvents() {
|
||||||
if (options.interactionSource) {
|
if (options.interactionSource) {
|
||||||
unsubscribeInteractionSource = options.interactionSource.subscribe(handleInteractionEvent)
|
unsubscribeInteractionSource = options.interactionSource.subscribe(presentationSpace, handleInteractionEvent)
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener('pointermove', handlePointerMove, { passive: true })
|
window.addEventListener('pointermove', handlePointerMove, { passive: true })
|
||||||
window.addEventListener('touchstart', handleTouchStart, { passive: true })
|
window.addEventListener('touchstart', handleTouchStart, { passive: true })
|
||||||
@@ -1813,8 +2053,13 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
window.addEventListener('touchcancel', handleTouchEnd, { passive: true })
|
window.addEventListener('touchcancel', handleTouchEnd, { passive: true })
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', resizeRenderer, { passive: true })
|
window.addEventListener('resize', resizeRenderer, { passive: true })
|
||||||
|
window.addEventListener('transitionrun', handleSurfaceTransitionRun, { capture: true, passive: true })
|
||||||
|
window.addEventListener('transitionend', handleSurfaceTransitionEnd, { capture: true, passive: true })
|
||||||
|
window.addEventListener('transitioncancel', handleSurfaceTransitionEnd, { capture: true, passive: true })
|
||||||
|
if (presentationSpace === 'scroll') {
|
||||||
window.addEventListener('scroll', handleScroll, { capture: true, passive: true })
|
window.addEventListener('scroll', handleScroll, { capture: true, passive: true })
|
||||||
if (presentationSpace === 'scroll') window.addEventListener('scrollend', handleScrollEnd, { passive: true })
|
window.addEventListener('scrollend', handleScrollEnd, { passive: true })
|
||||||
|
}
|
||||||
options.canvas.value?.addEventListener('webglcontextlost', handleContextLost)
|
options.canvas.value?.addEventListener('webglcontextlost', handleContextLost)
|
||||||
options.canvas.value?.addEventListener('webglcontextrestored', handleContextRestored)
|
options.canvas.value?.addEventListener('webglcontextrestored', handleContextRestored)
|
||||||
}
|
}
|
||||||
@@ -1830,32 +2075,39 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
window.removeEventListener('touchcancel', handleTouchEnd)
|
window.removeEventListener('touchcancel', handleTouchEnd)
|
||||||
}
|
}
|
||||||
window.removeEventListener('resize', resizeRenderer)
|
window.removeEventListener('resize', resizeRenderer)
|
||||||
|
window.removeEventListener('transitionrun', handleSurfaceTransitionRun, true)
|
||||||
|
window.removeEventListener('transitionend', handleSurfaceTransitionEnd, true)
|
||||||
|
window.removeEventListener('transitioncancel', handleSurfaceTransitionEnd, true)
|
||||||
|
if (presentationSpace === 'scroll') {
|
||||||
window.removeEventListener('scroll', handleScroll, true)
|
window.removeEventListener('scroll', handleScroll, true)
|
||||||
if (presentationSpace === 'scroll') window.removeEventListener('scrollend', handleScrollEnd)
|
window.removeEventListener('scrollend', handleScrollEnd)
|
||||||
|
}
|
||||||
options.canvas.value?.removeEventListener('webglcontextlost', handleContextLost)
|
options.canvas.value?.removeEventListener('webglcontextlost', handleContextLost)
|
||||||
options.canvas.value?.removeEventListener('webglcontextrestored', handleContextRestored)
|
options.canvas.value?.removeEventListener('webglcontextrestored', handleContextRestored)
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeRenderer(releaseContext = true) {
|
function disposeRenderer(releaseContext = true) {
|
||||||
loadVersion += 1
|
loadVersion += 1
|
||||||
|
prepareVersion += 1
|
||||||
contextRecoveryPending = false
|
contextRecoveryPending = false
|
||||||
interactionAnimating = false
|
interactionAnimating = false
|
||||||
cancelScheduledFrame()
|
cancelScheduledFrame()
|
||||||
cancelScrollFrame()
|
cancelScrollFrame()
|
||||||
cancelWallpaperTransitionFrame()
|
cancelWallpaperTransitionFrame()
|
||||||
|
cancelSurfaceTransformFrame()
|
||||||
clearBackgroundDisposeTimer()
|
clearBackgroundDisposeTimer()
|
||||||
if (surfaceUpdateFrame !== null) {
|
if (surfaceUpdateFrame !== null) {
|
||||||
cancelAnimationFrame(surfaceUpdateFrame)
|
cancelAnimationFrame(surfaceUpdateFrame)
|
||||||
surfaceUpdateFrame = null
|
surfaceUpdateFrame = null
|
||||||
}
|
}
|
||||||
|
if (surfaceStabilityFrame !== null) {
|
||||||
|
cancelAnimationFrame(surfaceStabilityFrame)
|
||||||
|
surfaceStabilityFrame = null
|
||||||
|
}
|
||||||
if (surfaceResizeTimer !== null) {
|
if (surfaceResizeTimer !== null) {
|
||||||
window.clearTimeout(surfaceResizeTimer)
|
window.clearTimeout(surfaceResizeTimer)
|
||||||
surfaceResizeTimer = null
|
surfaceResizeTimer = null
|
||||||
}
|
}
|
||||||
if (scrollSurfaceTimer !== null) {
|
|
||||||
window.clearTimeout(scrollSurfaceTimer)
|
|
||||||
scrollSurfaceTimer = null
|
|
||||||
}
|
|
||||||
removeEvents()
|
removeEvents()
|
||||||
resizeObserver?.disconnect()
|
resizeObserver?.disconnect()
|
||||||
resizeObserver = null
|
resizeObserver = null
|
||||||
@@ -1868,7 +2120,6 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
outgoingSurface = null
|
outgoingSurface = null
|
||||||
surfaceTransitionStartedAt = 0
|
surfaceTransitionStartedAt = 0
|
||||||
wakeDirection = { x: 0, y: -1 }
|
wakeDirection = { x: 0, y: -1 }
|
||||||
tracksScrollingSurfaces = false
|
|
||||||
document.documentElement.removeAttribute('data-glass-wallpaper-loading')
|
document.documentElement.removeAttribute('data-glass-wallpaper-loading')
|
||||||
activeTouchIdentifier = null
|
activeTouchIdentifier = null
|
||||||
lastPointerX = window.innerWidth * 0.5
|
lastPointerX = window.innerWidth * 0.5
|
||||||
@@ -1889,6 +2140,9 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
activeTexture = null
|
activeTexture = null
|
||||||
activeTextureHeight = 1
|
activeTextureHeight = 1
|
||||||
activeTextureWidth = 1
|
activeTextureWidth = 1
|
||||||
|
preparedWallpaper?.texture.dispose()
|
||||||
|
preparedWallpaper = null
|
||||||
|
preparedWallpaperUrl.value = ''
|
||||||
|
|
||||||
disposeFlowResources()
|
disposeFlowResources()
|
||||||
if (resources) {
|
if (resources) {
|
||||||
@@ -1981,9 +2235,9 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
resources.uniforms.uHasWallpaperTexture.value = hasWallpaperTexture ? 1 : 0
|
resources.uniforms.uHasWallpaperTexture.value = hasWallpaperTexture ? 1 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWallpaper(url: string, version: number) {
|
/** 解码并按当前质量预算缩放壁纸,不改变当前可见纹理。 */
|
||||||
if (!resources || !three || !url) return
|
async function createWallpaperTexture(url: string): Promise<PreparedWallpaperTexture | null> {
|
||||||
|
if (!resources || !three || !url) return null
|
||||||
const profile = getRenderProfile()
|
const profile = getRenderProfile()
|
||||||
const shouldUseProceduralTexture =
|
const shouldUseProceduralTexture =
|
||||||
profile.textureSource === 'procedural' ||
|
profile.textureSource === 'procedural' ||
|
||||||
@@ -1997,22 +2251,12 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
texture.generateMipmaps = false
|
texture.generateMipmaps = false
|
||||||
texture.minFilter = three.LinearFilter
|
texture.minFilter = three.LinearFilter
|
||||||
texture.magFilter = three.LinearFilter
|
texture.magFilter = three.LinearFilter
|
||||||
activateLoadedTexture(texture, 1, 1, false)
|
return { hasWallpaperTexture: false, height: 1, texture, width: 1 }
|
||||||
await resources.renderer.compileAsync(resources.scene, resources.camera)
|
|
||||||
if (version !== loadVersion || !resources) return
|
|
||||||
|
|
||||||
updateRendererState('ready')
|
|
||||||
scheduleFrame()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = new three.TextureLoader()
|
const loader = new three.TextureLoader()
|
||||||
loader.setCrossOrigin('anonymous')
|
loader.setCrossOrigin('anonymous')
|
||||||
const sourceTexture = await loader.loadAsync(url)
|
const sourceTexture = await loader.loadAsync(url)
|
||||||
if (version !== loadVersion || !resources) {
|
|
||||||
sourceTexture.dispose()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = sourceTexture.image as HTMLImageElement
|
const image = sourceTexture.image as HTMLImageElement
|
||||||
const sourceWidth = image.naturalWidth || image.width
|
const sourceWidth = image.naturalWidth || image.width
|
||||||
@@ -2041,7 +2285,57 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
texture.generateMipmaps = false
|
texture.generateMipmaps = false
|
||||||
texture.minFilter = three.LinearFilter
|
texture.minFilter = three.LinearFilter
|
||||||
texture.magFilter = three.LinearFilter
|
texture.magFilter = three.LinearFilter
|
||||||
activateLoadedTexture(texture, textureWidth, textureHeight, true)
|
return {
|
||||||
|
hasWallpaperTexture: true,
|
||||||
|
height: textureHeight,
|
||||||
|
texture,
|
||||||
|
width: textureWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提前准备下一张纹理;失败不会影响当前活动纹理。 */
|
||||||
|
async function prepareWallpaper(url: string) {
|
||||||
|
const version = ++prepareVersion
|
||||||
|
preparedWallpaper?.texture.dispose()
|
||||||
|
preparedWallpaper = null
|
||||||
|
preparedWallpaperUrl.value = ''
|
||||||
|
if (!url || !resources || url === toValue(options.wallpaperUrl)) {
|
||||||
|
if (url && activeTexture) preparedWallpaperUrl.value = url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prepared = await createWallpaperTexture(url)
|
||||||
|
if (!prepared) return
|
||||||
|
if (version !== prepareVersion || !resources) {
|
||||||
|
prepared.texture.dispose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resources.renderer.initTexture(prepared.texture)
|
||||||
|
preparedWallpaper = prepared
|
||||||
|
preparedWallpaperUrl.value = url
|
||||||
|
} catch (error) {
|
||||||
|
if (version === prepareVersion) console.warn('玻璃光学壁纸预备失败,继续使用当前纹理:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWallpaper(url: string, version: number) {
|
||||||
|
if (!resources || !three || !url) return
|
||||||
|
|
||||||
|
const prepared =
|
||||||
|
preparedWallpaper && preparedWallpaperUrl.value === url ? preparedWallpaper : await createWallpaperTexture(url)
|
||||||
|
if (!prepared) return
|
||||||
|
if (preparedWallpaper === prepared) {
|
||||||
|
preparedWallpaper = null
|
||||||
|
preparedWallpaperUrl.value = ''
|
||||||
|
}
|
||||||
|
if (version !== loadVersion || !resources) {
|
||||||
|
prepared.texture.dispose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activateLoadedTexture(prepared.texture, prepared.width, prepared.height, prepared.hasWallpaperTexture)
|
||||||
await resources.renderer.compileAsync(resources.scene, resources.camera)
|
await resources.renderer.compileAsync(resources.scene, resources.camera)
|
||||||
if (version !== loadVersion || !resources) return
|
if (version !== loadVersion || !resources) return
|
||||||
|
|
||||||
@@ -2104,6 +2398,11 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
uTextureMix: { value: 1 },
|
uTextureMix: { value: 1 },
|
||||||
uTintColor: { value: new three.Color(toValue(options.tintColor)) },
|
uTintColor: { value: new three.Color(toValue(options.tintColor)) },
|
||||||
uTransparency: { value: getTransparency() },
|
uTransparency: { value: getTransparency() },
|
||||||
|
uTransmissionStrength: {
|
||||||
|
value: getGlassOpticalTransmissionStrength(
|
||||||
|
toValue(options.transmissionStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT),
|
||||||
|
),
|
||||||
|
},
|
||||||
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: getRenderProfile().trailCount },
|
||||||
@@ -2165,26 +2464,34 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await nextTick()
|
if (resources && wallpaperUrl !== previous?.[1]) {
|
||||||
if (!resources) {
|
await refreshWallpaper('玻璃光学壁纸纹理加载失败,已保留当前材质:')
|
||||||
await initializeRenderer()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wallpaperUrl !== previous?.[1]) {
|
if (!resources) {
|
||||||
await refreshWallpaper('玻璃光学壁纸纹理加载失败,已保留当前材质:')
|
await nextTick()
|
||||||
|
await initializeRenderer()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => toValue(options.pendingWallpaperUrl ?? ''),
|
||||||
|
pendingWallpaperUrl => {
|
||||||
|
void prepareWallpaper(pendingWallpaperUrl)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => toValue(options.appearance),
|
() => toValue(options.appearance),
|
||||||
appearance => {
|
appearance => {
|
||||||
if (!resources) return
|
if (!resources) return
|
||||||
|
|
||||||
resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance)
|
resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance)
|
||||||
scheduleSurfaceUpdate()
|
scheduleSurfaceStabilityUpdate()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2234,8 +2541,16 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
toValue(options.flowStrength ?? getLegacyDynamicStrength()),
|
toValue(options.flowStrength ?? getLegacyDynamicStrength()),
|
||||||
toValue(options.reflectionStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT),
|
toValue(options.reflectionStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT),
|
||||||
toValue(options.transparencyStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT),
|
toValue(options.transparencyStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT),
|
||||||
|
toValue(options.transmissionStrength ?? GLASS_OPTICAL_STRENGTH_DEFAULT),
|
||||||
] as const,
|
] as const,
|
||||||
([translationStrength, deformationStrength, flowStrength, reflectionStrength, transparencyStrength]) => {
|
([
|
||||||
|
translationStrength,
|
||||||
|
deformationStrength,
|
||||||
|
flowStrength,
|
||||||
|
reflectionStrength,
|
||||||
|
transparencyStrength,
|
||||||
|
transmissionStrength,
|
||||||
|
]) => {
|
||||||
if (!resources) return
|
if (!resources) return
|
||||||
|
|
||||||
resources.uniforms.uTranslationStrength.value = getGlassOpticalTranslationStrengthScale(translationStrength)
|
resources.uniforms.uTranslationStrength.value = getGlassOpticalTranslationStrengthScale(translationStrength)
|
||||||
@@ -2248,6 +2563,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
)
|
)
|
||||||
resources.uniforms.uReflectionStrength.value = getGlassOpticalReflectionStrengthScale(reflectionStrength)
|
resources.uniforms.uReflectionStrength.value = getGlassOpticalReflectionStrengthScale(reflectionStrength)
|
||||||
resources.uniforms.uTransparency.value = getGlassOpticalTransparency(transparencyStrength)
|
resources.uniforms.uTransparency.value = getGlassOpticalTransparency(transparencyStrength)
|
||||||
|
resources.uniforms.uTransmissionStrength.value = getGlassOpticalTransmissionStrength(transmissionStrength)
|
||||||
if (resources.uniforms.uFlowStrength.value <= 0 && interactionAnimating) {
|
if (resources.uniforms.uFlowStrength.value <= 0 && interactionAnimating) {
|
||||||
interactionAnimating = false
|
interactionAnimating = false
|
||||||
cancelScheduledFrame()
|
cancelScheduledFrame()
|
||||||
@@ -2286,6 +2602,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
preparedWallpaperUrl,
|
||||||
renderedFrames,
|
renderedFrames,
|
||||||
state,
|
state,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import vuetify from '@/plugins/vuetify'
|
|||||||
import {
|
import {
|
||||||
GLASS_OPTICAL_STRENGTH_DEFAULT,
|
GLASS_OPTICAL_STRENGTH_DEFAULT,
|
||||||
GLASS_OPTICAL_STRENGTH_MAX,
|
GLASS_OPTICAL_STRENGTH_MAX,
|
||||||
|
getGlassOpticalCssTransmissionBrightness,
|
||||||
getGlassOpticalPresetParameters,
|
getGlassOpticalPresetParameters,
|
||||||
|
getGlassOpticalTransmissionStrength,
|
||||||
|
getGlassOpticalTransparency,
|
||||||
normalizeGlassOpticalStrength,
|
normalizeGlassOpticalStrength,
|
||||||
type GlassOpticalPreset,
|
type GlassOpticalPreset,
|
||||||
} from '@/utils/glassOptics'
|
} from '@/utils/glassOptics'
|
||||||
@@ -81,6 +84,8 @@ export interface ThemeCustomizerSettings {
|
|||||||
glassQuality: ThemeCustomizerGlassQuality
|
glassQuality: ThemeCustomizerGlassQuality
|
||||||
/** 玻璃亮边、镜面高光与焦散光照强度,范围 0 到 100。 */
|
/** 玻璃亮边、镜面高光与焦散光照强度,范围 0 到 100。 */
|
||||||
glassReflectionStrength: number
|
glassReflectionStrength: number
|
||||||
|
/** 玻璃内部壁纸采样的亮度与暗部展开强度,范围 0 到 100。 */
|
||||||
|
glassTransmissionStrength: number
|
||||||
/** 共享壁纸在表面内的统一采样平移强度,范围 0 到 100。 */
|
/** 共享壁纸在表面内的统一采样平移强度,范围 0 到 100。 */
|
||||||
glassTranslationStrength: number
|
glassTranslationStrength: number
|
||||||
/** 玻璃材质释放真实壁纸的程度,范围 0 到 100。 */
|
/** 玻璃材质释放真实壁纸的程度,范围 0 到 100。 */
|
||||||
@@ -152,6 +157,7 @@ function getDefaultThemeCustomizerSettings(): ThemeCustomizerSettings {
|
|||||||
glassPreset: 'natural',
|
glassPreset: 'natural',
|
||||||
glassQuality: defaultGlassQuality,
|
glassQuality: defaultGlassQuality,
|
||||||
glassReflectionStrength: glassParameters.reflection,
|
glassReflectionStrength: glassParameters.reflection,
|
||||||
|
glassTransmissionStrength: glassParameters.transmission,
|
||||||
glassTranslationStrength: glassParameters.translation,
|
glassTranslationStrength: glassParameters.translation,
|
||||||
glassTransparencyStrength: glassParameters.transparency,
|
glassTransparencyStrength: glassParameters.transparency,
|
||||||
layout: 'vertical',
|
layout: 'vertical',
|
||||||
@@ -186,7 +192,10 @@ function normalizeThemeCustomizerShadow(shadow: unknown): ThemeCustomizerShadow
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 规范化持久化的主题定制设置并迁移旧值。 */
|
/** 规范化持久化的主题定制设置并迁移旧值。 */
|
||||||
function normalizeThemeCustomizerSettings(settings: NormalizableThemeCustomizerSettings): ThemeCustomizerSettings {
|
function normalizeThemeCustomizerSettings(
|
||||||
|
settings: NormalizableThemeCustomizerSettings,
|
||||||
|
preserveLegacyTransmission = false,
|
||||||
|
): ThemeCustomizerSettings {
|
||||||
const fallback = getDefaultThemeCustomizerSettings()
|
const fallback = getDefaultThemeCustomizerSettings()
|
||||||
const storedRadius = settings.radius as string | undefined
|
const storedRadius = settings.radius as string | undefined
|
||||||
const radius = storedRadius === 'huge' ? 'extra' : storedRadius
|
const radius = storedRadius === 'huge' ? 'extra' : storedRadius
|
||||||
@@ -212,13 +221,25 @@ function normalizeThemeCustomizerSettings(settings: NormalizableThemeCustomizerS
|
|||||||
glassQuality: validGlassQualities.includes(settings.glassQuality as ThemeCustomizerGlassQuality)
|
glassQuality: validGlassQualities.includes(settings.glassQuality as ThemeCustomizerGlassQuality)
|
||||||
? (settings.glassQuality as ThemeCustomizerGlassQuality)
|
? (settings.glassQuality as ThemeCustomizerGlassQuality)
|
||||||
: fallback.glassQuality,
|
: fallback.glassQuality,
|
||||||
glassReflectionStrength: normalizeGlassOpticalStrength(settings.glassReflectionStrength),
|
glassReflectionStrength:
|
||||||
|
settings.glassReflectionStrength === undefined
|
||||||
|
? fallback.glassReflectionStrength
|
||||||
|
: normalizeGlassOpticalStrength(settings.glassReflectionStrength),
|
||||||
|
glassTransmissionStrength:
|
||||||
|
settings.glassTransmissionStrength === undefined
|
||||||
|
? preserveLegacyTransmission
|
||||||
|
? GLASS_OPTICAL_STRENGTH_DEFAULT
|
||||||
|
: fallback.glassTransmissionStrength
|
||||||
|
: normalizeGlassOpticalStrength(settings.glassTransmissionStrength),
|
||||||
glassTranslationStrength: normalizeMigratedGlassStrength(
|
glassTranslationStrength: normalizeMigratedGlassStrength(
|
||||||
settings.glassTranslationStrength,
|
settings.glassTranslationStrength,
|
||||||
settings.glassMotionStrength,
|
settings.glassMotionStrength,
|
||||||
fallback.glassTranslationStrength,
|
fallback.glassTranslationStrength,
|
||||||
),
|
),
|
||||||
glassTransparencyStrength: normalizeGlassOpticalStrength(settings.glassTransparencyStrength),
|
glassTransparencyStrength:
|
||||||
|
settings.glassTransparencyStrength === undefined
|
||||||
|
? fallback.glassTransparencyStrength
|
||||||
|
: normalizeGlassOpticalStrength(settings.glassTransparencyStrength),
|
||||||
layout: validLayouts.includes(settings.layout as ThemeCustomizerLayout)
|
layout: validLayouts.includes(settings.layout as ThemeCustomizerLayout)
|
||||||
? (settings.layout as ThemeCustomizerLayout)
|
? (settings.layout as ThemeCustomizerLayout)
|
||||||
: fallback.layout,
|
: fallback.layout,
|
||||||
@@ -244,7 +265,7 @@ export function readThemeCustomizerSettings(): ThemeCustomizerSettings {
|
|||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(THEME_CUSTOMIZER_STORAGE_KEY)
|
const stored = localStorage.getItem(THEME_CUSTOMIZER_STORAGE_KEY)
|
||||||
const parsed = stored ? JSON.parse(stored) : {}
|
const parsed = stored ? JSON.parse(stored) : {}
|
||||||
return normalizeThemeCustomizerSettings({ ...parsed, theme: readStoredThemePreference() })
|
return normalizeThemeCustomizerSettings({ ...parsed, theme: readStoredThemePreference() }, Boolean(stored))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('读取主题定制设置失败,已使用默认设置:', error)
|
console.warn('读取主题定制设置失败,已使用默认设置:', error)
|
||||||
|
|
||||||
@@ -262,6 +283,7 @@ type ThemeCustomizerGlassSettings = Pick<
|
|||||||
| 'glassPreset'
|
| 'glassPreset'
|
||||||
| 'glassQuality'
|
| 'glassQuality'
|
||||||
| 'glassReflectionStrength'
|
| 'glassReflectionStrength'
|
||||||
|
| 'glassTransmissionStrength'
|
||||||
| 'glassTranslationStrength'
|
| 'glassTranslationStrength'
|
||||||
| 'glassTransparencyStrength'
|
| 'glassTransparencyStrength'
|
||||||
>
|
>
|
||||||
@@ -275,6 +297,8 @@ const effectiveGlassSettings = computed(() => ({
|
|||||||
glassQuality: glassPreviewState.value?.glassQuality ?? settingsState.value.glassQuality,
|
glassQuality: glassPreviewState.value?.glassQuality ?? settingsState.value.glassQuality,
|
||||||
glassReflectionStrength:
|
glassReflectionStrength:
|
||||||
glassPreviewState.value?.glassReflectionStrength ?? settingsState.value.glassReflectionStrength,
|
glassPreviewState.value?.glassReflectionStrength ?? settingsState.value.glassReflectionStrength,
|
||||||
|
glassTransmissionStrength:
|
||||||
|
glassPreviewState.value?.glassTransmissionStrength ?? settingsState.value.glassTransmissionStrength,
|
||||||
glassTranslationStrength:
|
glassTranslationStrength:
|
||||||
glassPreviewState.value?.glassTranslationStrength ?? settingsState.value.glassTranslationStrength,
|
glassPreviewState.value?.glassTranslationStrength ?? settingsState.value.glassTranslationStrength,
|
||||||
glassTransparencyStrength:
|
glassTransparencyStrength:
|
||||||
@@ -340,6 +364,7 @@ export function applyThemeCustomizerRootSettings(
|
|||||||
| 'glassAppearance'
|
| 'glassAppearance'
|
||||||
| 'glassQuality'
|
| 'glassQuality'
|
||||||
| 'glassReflectionStrength'
|
| 'glassReflectionStrength'
|
||||||
|
| 'glassTransmissionStrength'
|
||||||
| 'glassTransparencyStrength'
|
| 'glassTransparencyStrength'
|
||||||
| 'layout'
|
| 'layout'
|
||||||
| 'radius'
|
| 'radius'
|
||||||
@@ -356,9 +381,17 @@ export function applyThemeCustomizerRootSettings(
|
|||||||
'--glass-reflection',
|
'--glass-reflection',
|
||||||
String(normalizeGlassOpticalStrength(settings.glassReflectionStrength) / GLASS_OPTICAL_STRENGTH_MAX),
|
String(normalizeGlassOpticalStrength(settings.glassReflectionStrength) / GLASS_OPTICAL_STRENGTH_MAX),
|
||||||
)
|
)
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--glass-transmission',
|
||||||
|
String(getGlassOpticalTransmissionStrength(settings.glassTransmissionStrength)),
|
||||||
|
)
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--glass-transmission-brightness',
|
||||||
|
String(getGlassOpticalCssTransmissionBrightness(settings.glassTransmissionStrength)),
|
||||||
|
)
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
'--glass-transparency',
|
'--glass-transparency',
|
||||||
String(normalizeGlassOpticalStrength(settings.glassTransparencyStrength) / GLASS_OPTICAL_STRENGTH_MAX),
|
String(getGlassOpticalTransparency(settings.glassTransparencyStrength)),
|
||||||
)
|
)
|
||||||
document.documentElement.setAttribute('data-theme-layout', settings.layout)
|
document.documentElement.setAttribute('data-theme-layout', settings.layout)
|
||||||
document.documentElement.setAttribute('data-theme-radius', settings.radius)
|
document.documentElement.setAttribute('data-theme-radius', settings.radius)
|
||||||
@@ -371,9 +404,17 @@ export function applyThemeCustomizerRootSettings(
|
|||||||
'--glass-reflection',
|
'--glass-reflection',
|
||||||
String(normalizeGlassOpticalStrength(settings.glassReflectionStrength) / GLASS_OPTICAL_STRENGTH_MAX),
|
String(normalizeGlassOpticalStrength(settings.glassReflectionStrength) / GLASS_OPTICAL_STRENGTH_MAX),
|
||||||
)
|
)
|
||||||
|
document.body.style.setProperty(
|
||||||
|
'--glass-transmission',
|
||||||
|
String(getGlassOpticalTransmissionStrength(settings.glassTransmissionStrength)),
|
||||||
|
)
|
||||||
|
document.body.style.setProperty(
|
||||||
|
'--glass-transmission-brightness',
|
||||||
|
String(getGlassOpticalCssTransmissionBrightness(settings.glassTransmissionStrength)),
|
||||||
|
)
|
||||||
document.body.style.setProperty(
|
document.body.style.setProperty(
|
||||||
'--glass-transparency',
|
'--glass-transparency',
|
||||||
String(normalizeGlassOpticalStrength(settings.glassTransparencyStrength) / GLASS_OPTICAL_STRENGTH_MAX),
|
String(getGlassOpticalTransparency(settings.glassTransparencyStrength)),
|
||||||
)
|
)
|
||||||
document.body.setAttribute('data-theme-layout', settings.layout)
|
document.body.setAttribute('data-theme-layout', settings.layout)
|
||||||
document.body.setAttribute('data-theme-radius', settings.radius)
|
document.body.setAttribute('data-theme-radius', settings.radius)
|
||||||
@@ -457,6 +498,7 @@ export function previewGlassSettings(patch: Partial<ThemeCustomizerGlassSettings
|
|||||||
glassPreset: previewSettings.glassPreset,
|
glassPreset: previewSettings.glassPreset,
|
||||||
glassQuality: previewSettings.glassQuality,
|
glassQuality: previewSettings.glassQuality,
|
||||||
glassReflectionStrength: previewSettings.glassReflectionStrength,
|
glassReflectionStrength: previewSettings.glassReflectionStrength,
|
||||||
|
glassTransmissionStrength: previewSettings.glassTransmissionStrength,
|
||||||
glassTranslationStrength: previewSettings.glassTranslationStrength,
|
glassTranslationStrength: previewSettings.glassTranslationStrength,
|
||||||
glassTransparencyStrength: previewSettings.glassTransparencyStrength,
|
glassTransparencyStrength: previewSettings.glassTransparencyStrength,
|
||||||
}
|
}
|
||||||
@@ -491,15 +533,17 @@ export function cancelGlassPreview() {
|
|||||||
|
|
||||||
/** 判断当前主题定制设置是否仍为默认值。 */
|
/** 判断当前主题定制设置是否仍为默认值。 */
|
||||||
export function isDefaultThemeCustomizerSettings(settings: ThemeCustomizerSettings) {
|
export function isDefaultThemeCustomizerSettings(settings: ThemeCustomizerSettings) {
|
||||||
|
const glassParameters = getGlassOpticalPresetParameters('clear', defaultGlassQuality, 'natural')
|
||||||
const defaults = normalizeThemeCustomizerSettings({
|
const defaults = normalizeThemeCustomizerSettings({
|
||||||
glassAppearance: 'clear',
|
glassAppearance: 'clear',
|
||||||
glassDeformationStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassDeformationStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
||||||
glassFlowStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassFlowStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
||||||
glassPreset: 'natural',
|
glassPreset: 'natural',
|
||||||
glassQuality: defaultGlassQuality,
|
glassQuality: defaultGlassQuality,
|
||||||
glassReflectionStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassReflectionStrength: glassParameters.reflection,
|
||||||
|
glassTransmissionStrength: glassParameters.transmission,
|
||||||
glassTranslationStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassTranslationStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
||||||
glassTransparencyStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassTransparencyStrength: glassParameters.transparency,
|
||||||
layout: 'vertical',
|
layout: 'vertical',
|
||||||
primaryColor: defaultPrimaryColor,
|
primaryColor: defaultPrimaryColor,
|
||||||
radius: 'default',
|
radius: 'default',
|
||||||
@@ -516,6 +560,7 @@ export function isDefaultThemeCustomizerSettings(settings: ThemeCustomizerSettin
|
|||||||
settings.glassPreset === defaults.glassPreset &&
|
settings.glassPreset === defaults.glassPreset &&
|
||||||
settings.glassQuality === defaults.glassQuality &&
|
settings.glassQuality === defaults.glassQuality &&
|
||||||
settings.glassReflectionStrength === defaults.glassReflectionStrength &&
|
settings.glassReflectionStrength === defaults.glassReflectionStrength &&
|
||||||
|
settings.glassTransmissionStrength === defaults.glassTransmissionStrength &&
|
||||||
settings.glassTranslationStrength === defaults.glassTranslationStrength &&
|
settings.glassTranslationStrength === defaults.glassTranslationStrength &&
|
||||||
settings.glassTransparencyStrength === defaults.glassTransparencyStrength &&
|
settings.glassTransparencyStrength === defaults.glassTransparencyStrength &&
|
||||||
settings.layout === defaults.layout &&
|
settings.layout === defaults.layout &&
|
||||||
@@ -592,6 +637,11 @@ export function useThemeCustomizer() {
|
|||||||
return updateSettings({ glassReflectionStrength })
|
return updateSettings({ glassReflectionStrength })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更新玻璃内部壁纸采样的透射亮度。 */
|
||||||
|
function setGlassTransmissionStrength(glassTransmissionStrength: number) {
|
||||||
|
return updateSettings({ glassTransmissionStrength })
|
||||||
|
}
|
||||||
|
|
||||||
/** 更新玻璃统一采样平移强度。 */
|
/** 更新玻璃统一采样平移强度。 */
|
||||||
function setGlassTranslationStrength(glassTranslationStrength: number) {
|
function setGlassTranslationStrength(glassTranslationStrength: number) {
|
||||||
return updateSettings({ glassTranslationStrength })
|
return updateSettings({ glassTranslationStrength })
|
||||||
@@ -634,15 +684,18 @@ export function useThemeCustomizer() {
|
|||||||
|
|
||||||
/** 将主题定制器恢复到默认设置。 */
|
/** 将主题定制器恢复到默认设置。 */
|
||||||
async function resetSettings() {
|
async function resetSettings() {
|
||||||
|
const glassParameters = getGlassOpticalPresetParameters('clear', defaultGlassQuality, 'natural')
|
||||||
|
|
||||||
await updateSettings({
|
await updateSettings({
|
||||||
glassAppearance: 'clear',
|
glassAppearance: 'clear',
|
||||||
glassDeformationStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassDeformationStrength: glassParameters.deformation,
|
||||||
glassFlowStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassFlowStrength: glassParameters.flow,
|
||||||
glassPreset: 'natural',
|
glassPreset: 'natural',
|
||||||
glassQuality: defaultGlassQuality,
|
glassQuality: defaultGlassQuality,
|
||||||
glassReflectionStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassReflectionStrength: glassParameters.reflection,
|
||||||
glassTranslationStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassTransmissionStrength: glassParameters.transmission,
|
||||||
glassTransparencyStrength: GLASS_OPTICAL_STRENGTH_DEFAULT,
|
glassTranslationStrength: glassParameters.translation,
|
||||||
|
glassTransparencyStrength: glassParameters.transparency,
|
||||||
layout: 'vertical',
|
layout: 'vertical',
|
||||||
primaryColor: defaultPrimaryColor,
|
primaryColor: defaultPrimaryColor,
|
||||||
radius: 'default',
|
radius: 'default',
|
||||||
@@ -684,6 +737,7 @@ export function useThemeCustomizer() {
|
|||||||
setGlassPreset,
|
setGlassPreset,
|
||||||
setGlassQuality,
|
setGlassQuality,
|
||||||
setGlassReflectionStrength,
|
setGlassReflectionStrength,
|
||||||
|
setGlassTransmissionStrength,
|
||||||
setGlassTranslationStrength,
|
setGlassTranslationStrength,
|
||||||
setGlassTransparencyStrength,
|
setGlassTransparencyStrength,
|
||||||
setLayout,
|
setLayout,
|
||||||
|
|||||||
@@ -156,22 +156,26 @@ export default {
|
|||||||
glassQualityCss: 'Standard',
|
glassQualityCss: 'Standard',
|
||||||
glassQualityBalanced: 'Balanced',
|
glassQualityBalanced: 'Balanced',
|
||||||
glassQualityHigh: 'High',
|
glassQualityHigh: 'High',
|
||||||
glassQualityCssHint: 'Static CSS material with the lowest resource use; clarity and reflection remain available.',
|
glassQualityCssHint:
|
||||||
|
'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.',
|
||||||
glassPreset: 'Preset',
|
glassPreset: 'Preset',
|
||||||
glassPresetNatural: 'Natural',
|
glassPresetNatural: 'Natural',
|
||||||
glassPresetGlide: 'Glide',
|
glassPresetGlide: 'Glide',
|
||||||
glassPresetLiquid: 'Liquid',
|
glassPresetLiquid: 'Liquid',
|
||||||
|
glassMaterialTuning: 'Material',
|
||||||
|
glassDynamicTuning: 'Motion',
|
||||||
glassTranslationStrength: 'Sample Translation',
|
glassTranslationStrength: 'Sample Translation',
|
||||||
glassDeformationStrength: 'Deformation',
|
glassDeformationStrength: 'Deformation',
|
||||||
glassFlowStrength: 'Flow Strength',
|
glassFlowStrength: 'Flow Strength',
|
||||||
glassReflectionStrength: 'Reflection Brightness',
|
glassReflectionStrength: 'Reflection Brightness',
|
||||||
|
glassTransmissionStrength: 'Transmission Brightness',
|
||||||
glassTransparencyStrength: 'Clarity',
|
glassTransparencyStrength: 'Clarity',
|
||||||
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:
|
glassOpticalStrengthUnavailableHint:
|
||||||
'Standard quality provides clarity and static reflection only. Switch to Balanced or High to adjust sample translation, deformation, and flow.',
|
'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',
|
||||||
|
|||||||
@@ -154,21 +154,23 @@ export default {
|
|||||||
glassQualityCss: '标准',
|
glassQualityCss: '标准',
|
||||||
glassQualityBalanced: '均衡',
|
glassQualityBalanced: '均衡',
|
||||||
glassQualityHigh: '高质量',
|
glassQualityHigh: '高质量',
|
||||||
glassQualityCssHint: '静态 CSS 材质,资源占用最低,保留通透与反射,不启用实时流动。',
|
glassQualityCssHint: '静态 CSS 材质,资源占用最低,保留通透、透射与反射,不启用实时流动。',
|
||||||
glassQualityBalancedHint: '共享实时折射,优先平衡流动反馈与 GPU 占用。',
|
glassQualityBalancedHint: '共享实时折射,优先平衡流动反馈与 GPU 占用。',
|
||||||
glassQualityHighHint: '完整时序流场、扩散细节与内容保护,GPU 占用更高。',
|
glassQualityHighHint: '完整时序流场、扩散细节与内容保护,GPU 占用更高。',
|
||||||
glassPreset: '方案',
|
glassPreset: '方案',
|
||||||
glassPresetNatural: '自然',
|
glassPresetNatural: '自然',
|
||||||
glassPresetGlide: '滑移',
|
glassPresetGlide: '滑移',
|
||||||
glassPresetLiquid: '液态',
|
glassPresetLiquid: '液态',
|
||||||
|
glassMaterialTuning: '材质参数',
|
||||||
|
glassDynamicTuning: '动态参数',
|
||||||
glassTranslationStrength: '采样平移',
|
glassTranslationStrength: '采样平移',
|
||||||
glassDeformationStrength: '形变强度',
|
glassDeformationStrength: '形变强度',
|
||||||
glassFlowStrength: '流动强度',
|
glassFlowStrength: '流动强度',
|
||||||
glassReflectionStrength: '反射亮度',
|
glassReflectionStrength: '反射亮度',
|
||||||
|
glassTransmissionStrength: '透射亮度',
|
||||||
glassTransparencyStrength: '通透度',
|
glassTransparencyStrength: '通透度',
|
||||||
glassOpticalStrengthHint: '采样平移控制整体滑移,形变强度控制局部弯曲,流动强度控制轨迹范围与惯性。',
|
glassOpticalStrengthHint: '采样平移控制整体滑移,形变强度控制局部弯曲,流动强度控制轨迹范围与惯性。',
|
||||||
glassOpticalStrengthUnavailableHint:
|
glassOpticalStrengthUnavailableHint: '标准质量保留三项材质参数。切换到均衡或高质量后,可调整采样平移、形变和流动。',
|
||||||
'标准质量仅提供通透度和静态反射。切换到均衡或高质量后,可调整采样平移、形变和流动。',
|
|
||||||
purple: '幻紫',
|
purple: '幻紫',
|
||||||
custom: '附加样式',
|
custom: '附加样式',
|
||||||
transparency: '透明度',
|
transparency: '透明度',
|
||||||
|
|||||||
@@ -154,21 +154,23 @@ export default {
|
|||||||
glassQualityCss: '標準',
|
glassQualityCss: '標準',
|
||||||
glassQualityBalanced: '均衡',
|
glassQualityBalanced: '均衡',
|
||||||
glassQualityHigh: '高品質',
|
glassQualityHigh: '高品質',
|
||||||
glassQualityCssHint: '靜態 CSS 材質,資源佔用最低,保留通透與反射,不啟用即時流動。',
|
glassQualityCssHint: '靜態 CSS 材質,資源佔用最低,保留通透、透射與反射,不啟用即時流動。',
|
||||||
glassQualityBalancedHint: '共享即時折射,優先平衡流動回饋與 GPU 佔用。',
|
glassQualityBalancedHint: '共享即時折射,優先平衡流動回饋與 GPU 佔用。',
|
||||||
glassQualityHighHint: '完整時序流場、擴散細節與內容保護,GPU 佔用更高。',
|
glassQualityHighHint: '完整時序流場、擴散細節與內容保護,GPU 佔用更高。',
|
||||||
glassPreset: '方案',
|
glassPreset: '方案',
|
||||||
glassPresetNatural: '自然',
|
glassPresetNatural: '自然',
|
||||||
glassPresetGlide: '滑移',
|
glassPresetGlide: '滑移',
|
||||||
glassPresetLiquid: '液態',
|
glassPresetLiquid: '液態',
|
||||||
|
glassMaterialTuning: '材質參數',
|
||||||
|
glassDynamicTuning: '動態參數',
|
||||||
glassTranslationStrength: '採樣平移',
|
glassTranslationStrength: '採樣平移',
|
||||||
glassDeformationStrength: '形變強度',
|
glassDeformationStrength: '形變強度',
|
||||||
glassFlowStrength: '流動強度',
|
glassFlowStrength: '流動強度',
|
||||||
glassReflectionStrength: '反射亮度',
|
glassReflectionStrength: '反射亮度',
|
||||||
|
glassTransmissionStrength: '透射亮度',
|
||||||
glassTransparencyStrength: '通透度',
|
glassTransparencyStrength: '通透度',
|
||||||
glassOpticalStrengthHint: '採樣平移控制整體滑移,形變強度控制局部彎曲,流動強度控制軌跡範圍與慣性。',
|
glassOpticalStrengthHint: '採樣平移控制整體滑移,形變強度控制局部彎曲,流動強度控制軌跡範圍與慣性。',
|
||||||
glassOpticalStrengthUnavailableHint:
|
glassOpticalStrengthUnavailableHint: '標準品質保留三項材質參數。切換到均衡或高品質後,可調整採樣平移、形變和流動。',
|
||||||
'標準品質僅提供通透度和靜態反射。切換到均衡或高品質後,可調整採樣平移、形變和流動。',
|
|
||||||
purple: '幻紫',
|
purple: '幻紫',
|
||||||
custom: '附加樣式',
|
custom: '附加樣式',
|
||||||
transparency: '透明度',
|
transparency: '透明度',
|
||||||
|
|||||||
+60
-303
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AxiosError } from 'axios'
|
import type { AxiosError } from 'axios'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
|
import { useTheme } from 'vuetify'
|
||||||
import { useAuthStore, useUserStore } from '@/stores'
|
import { useAuthStore, useUserStore } from '@/stores'
|
||||||
import { authState, userState } from '@/stores/types'
|
import { authState, userState } from '@/stores/types'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
@@ -15,8 +16,8 @@ import { buildUserPermissionContext, filterMenusByPermission } from '@/utils/per
|
|||||||
import type { ApiResponse } from '@/api/types'
|
import type { ApiResponse } from '@/api/types'
|
||||||
import { loadRemoteComponentFromModule, type RemoteModule } from '@/utils/federationLoader'
|
import { loadRemoteComponentFromModule, type RemoteModule } from '@/utils/federationLoader'
|
||||||
import type { MfaMethod } from '@/types/auth'
|
import type { MfaMethod } from '@/types/auth'
|
||||||
|
import { getLoginVisualProfile } from '@/utils/loginPresentation'
|
||||||
|
|
||||||
const loginRootRef = ref<HTMLElement | null>(null)
|
|
||||||
type LabTapTarget = 'logo' | 'title'
|
type LabTapTarget = 'logo' | 'title'
|
||||||
|
|
||||||
const LAB_TAP_COUNT = 5
|
const LAB_TAP_COUNT = 5
|
||||||
@@ -25,10 +26,8 @@ const labTapSequences: Record<LabTapTarget, { count: number; startedAt: number }
|
|||||||
logo: { count: 0, startedAt: 0 },
|
logo: { count: 0, startedAt: 0 },
|
||||||
title: { count: 0, startedAt: 0 },
|
title: { count: 0, startedAt: 0 },
|
||||||
}
|
}
|
||||||
let cardLightFrame: number | null = null
|
const { global: loginTheme } = useTheme()
|
||||||
let pendingCardLightX = 0.5
|
const loginVisualProfile = computed(() => getLoginVisualProfile(loginTheme.name.value))
|
||||||
let pendingCardLightY = 0
|
|
||||||
let pendingCardLightEnergy = 0
|
|
||||||
|
|
||||||
/** 在指定区域连续点击五次时进入隐藏的 Logo 实验室。 */
|
/** 在指定区域连续点击五次时进入隐藏的 Logo 实验室。 */
|
||||||
function handleLabTap(target: LabTapTarget) {
|
function handleLabTap(target: LabTapTarget) {
|
||||||
@@ -53,40 +52,6 @@ function handleLabTap(target: LabTapTarget) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 卡片顶部反射与指针共用光源位置,避免通过响应式状态触发页面重渲染。 */
|
|
||||||
function renderCardLight() {
|
|
||||||
cardLightFrame = null
|
|
||||||
const root = loginRootRef.value
|
|
||||||
root?.style.setProperty('--login-card-light-x', `${(pendingCardLightX * 100).toFixed(2)}%`)
|
|
||||||
root?.style.setProperty('--login-card-light-y', `${(pendingCardLightY * 100).toFixed(2)}%`)
|
|
||||||
root?.style.setProperty('--login-card-highlight-alpha', (0.035 + pendingCardLightEnergy * 0.08).toFixed(3))
|
|
||||||
root?.style.setProperty('--login-card-primary-alpha', (0.018 + pendingCardLightEnergy * 0.04).toFixed(3))
|
|
||||||
root?.style.setProperty('--login-card-top-prism-alpha', (0.2 + pendingCardLightEnergy * 0.32).toFixed(3))
|
|
||||||
root?.style.setProperty('--login-refraction-shift-x', `${((0.5 - pendingCardLightX) * 8).toFixed(2)}px`)
|
|
||||||
root?.style.setProperty('--login-refraction-shift-y', `${((0.5 - pendingCardLightY) * 6).toFixed(2)}px`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 根据指针在登录卡片中的位置更新光照目标。 */
|
|
||||||
function handlePointerLight(event: PointerEvent) {
|
|
||||||
if (event.pointerType === 'touch') return
|
|
||||||
const bounds = loginRootRef.value?.querySelector<HTMLElement>('.login-card')?.getBoundingClientRect()
|
|
||||||
if (!bounds?.width || !bounds.height) return
|
|
||||||
|
|
||||||
pendingCardLightX = Math.min(1, Math.max(0, (event.clientX - bounds.left) / bounds.width))
|
|
||||||
pendingCardLightY = Math.min(1, Math.max(0, (event.clientY - bounds.top) / bounds.height))
|
|
||||||
const centerDistance = Math.hypot(pendingCardLightX - 0.5, pendingCardLightY - 0.5)
|
|
||||||
pendingCardLightEnergy = Math.max(0.16, 1 - centerDistance * 1.2)
|
|
||||||
if (cardLightFrame === null) cardLightFrame = window.requestAnimationFrame(renderCardLight)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 指针离开登录页后恢复卡片的默认光照位置。 */
|
|
||||||
function resetPointerLight() {
|
|
||||||
pendingCardLightX = 0.5
|
|
||||||
pendingCardLightY = 0
|
|
||||||
pendingCardLightEnergy = 0
|
|
||||||
if (cardLightFrame === null) cardLightFrame = window.requestAnimationFrame(renderCardLight)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t, te } = useI18n()
|
const { t, te } = useI18n()
|
||||||
|
|
||||||
@@ -766,8 +731,6 @@ async function initConditionalPasskey() {
|
|||||||
|
|
||||||
// 组件卸载时清理
|
// 组件卸载时清理
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (cardLightFrame !== null) window.cancelAnimationFrame(cardLightFrame)
|
|
||||||
|
|
||||||
if (conditionalAbortController) {
|
if (conditionalAbortController) {
|
||||||
conditionalAbortController.abort()
|
conditionalAbortController.abort()
|
||||||
conditionalAbortController = null
|
conditionalAbortController = null
|
||||||
@@ -781,30 +744,10 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 登录页面容器 -->
|
<!-- 登录页面容器 -->
|
||||||
<div ref="loginRootRef" class="login-root" @pointermove="handlePointerLight" @pointerleave="resetPointerLight">
|
<div class="login-root" :data-login-visual-profile="loginVisualProfile">
|
||||||
<!-- SVG 位移只处理卡片内重复绘制的壁纸,不读取跨域图片像素。 -->
|
<!-- 经典主题保留一层低频品牌环境光;透明与玻璃 profile 不挂载该装饰。 -->
|
||||||
<svg class="login-liquid-filter-defs" aria-hidden="true" focusable="false">
|
<div v-if="loginVisualProfile === 'classic'" class="login-ambient-light" aria-hidden="true">
|
||||||
<defs>
|
<span class="login-ambient-light__wash" />
|
||||||
<filter
|
|
||||||
id="login-liquid-refraction-clear"
|
|
||||||
x="-8%"
|
|
||||||
y="-8%"
|
|
||||||
width="116%"
|
|
||||||
height="116%"
|
|
||||||
color-interpolation-filters="sRGB"
|
|
||||||
>
|
|
||||||
<feTurbulence type="fractalNoise" baseFrequency="0.009 0.014" numOctaves="2" seed="7" result="noise" />
|
|
||||||
<feGaussianBlur in="noise" stdDeviation="1.2" result="soft-noise" />
|
|
||||||
<feDisplacementMap in="SourceGraphic" in2="soft-noise" scale="24" xChannelSelector="R" yChannelSelector="G" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- 装饰性背景光晕 -->
|
|
||||||
<div class="login-bg-decor" aria-hidden="true">
|
|
||||||
<div class="login-orb login-orb--1" />
|
|
||||||
<div class="login-orb login-orb--2" />
|
|
||||||
<div class="login-orb login-orb--3" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 顶部漂浮语言切换 -->
|
<!-- 顶部漂浮语言切换 -->
|
||||||
@@ -841,10 +784,7 @@ onUnmounted(() => {
|
|||||||
max-width="24rem"
|
max-width="24rem"
|
||||||
flat
|
flat
|
||||||
>
|
>
|
||||||
<div class="login-card__glass" aria-hidden="true">
|
<div class="login-card__surface" aria-hidden="true" />
|
||||||
<span class="login-card__wallpaper-refraction" />
|
|
||||||
<span class="login-card__glass-caustic" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 卡片头部:Logo + 标题 + 欢迎语 -->
|
<!-- 卡片头部:Logo + 标题 + 欢迎语 -->
|
||||||
<div class="login-head">
|
<div class="login-head">
|
||||||
@@ -1023,34 +963,10 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* ===================== 布局根容器 ===================== */
|
/* ===================== 布局根容器 ===================== */
|
||||||
.login-root {
|
.login-root {
|
||||||
--login-card-light-x: 50%;
|
|
||||||
--login-card-light-y: 0%;
|
|
||||||
--login-card-highlight-alpha: 0.035;
|
|
||||||
--login-card-primary-alpha: 0.018;
|
|
||||||
--login-card-top-prism-alpha: 0.2;
|
|
||||||
--login-refraction-shift-x: 0px;
|
|
||||||
--login-refraction-shift-y: 3px;
|
|
||||||
--optical-glass-x: 50%;
|
|
||||||
--optical-glass-y: 22%;
|
|
||||||
--optical-glass-blur: 24px;
|
|
||||||
--optical-glass-saturate: 146%;
|
|
||||||
--optical-glass-contrast: 104%;
|
|
||||||
--optical-glass-glow-opacity: 0.36;
|
|
||||||
--optical-glass-caustic-opacity: 0.28;
|
|
||||||
--optical-glass-caustic-scale: 0.9;
|
|
||||||
--optical-glass-pulse-blur: 14px;
|
|
||||||
--optical-glass-shift-x: 0px;
|
|
||||||
--optical-glass-shift-y: 0px;
|
|
||||||
--optical-glass-rotation: -5deg;
|
|
||||||
--optical-glass-scale: 1;
|
|
||||||
--optical-glass-caustic-highlight-alpha: 0.16;
|
|
||||||
--optical-glass-caustic-primary-alpha: 0.12;
|
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-x: hidden;
|
overflow-x: clip;
|
||||||
overflow-y: auto;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -1060,15 +976,8 @@ onUnmounted(() => {
|
|||||||
padding-block: calc(env(safe-area-inset-top, 0px) + 24px) calc(env(safe-area-inset-bottom, 0px) + 24px);
|
padding-block: calc(env(safe-area-inset-top, 0px) + 24px) calc(env(safe-area-inset-bottom, 0px) + 24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-liquid-filter-defs {
|
/* 经典 profile 只保留一层覆盖视口的低频品牌环境光。 */
|
||||||
position: absolute;
|
.login-ambient-light {
|
||||||
block-size: 0;
|
|
||||||
inline-size: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================== 装饰性背景光晕 ===================== */
|
|
||||||
.login-bg-decor {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1076,69 +985,20 @@ onUnmounted(() => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-orb {
|
.login-ambient-light__wash {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
animation: ambient-drift 18s ease-in-out infinite alternate;
|
||||||
will-change: transform;
|
background:
|
||||||
|
radial-gradient(ellipse at 78% 18%, rgba(var(--v-theme-primary), 0.18), transparent 48%),
|
||||||
|
linear-gradient(145deg, transparent 28%, rgba(var(--v-theme-primary), 0.07) 72%, transparent);
|
||||||
|
filter: blur(40px);
|
||||||
|
inset: -12%;
|
||||||
|
transform: translate3d(1.5%, -1%, 0) scale(1.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-orb--1 {
|
@keyframes ambient-drift {
|
||||||
animation: orb-float-1 12s ease-in-out infinite alternate;
|
to {
|
||||||
background: rgba(var(--v-theme-primary), 0.35);
|
transform: translate3d(-1.5%, 1%, 0) scale(1);
|
||||||
block-size: 360px;
|
|
||||||
filter: blur(60px);
|
|
||||||
inline-size: 360px;
|
|
||||||
inset-block-start: -15%;
|
|
||||||
inset-inline-end: -12%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-orb--2 {
|
|
||||||
animation: orb-float-2 15s ease-in-out infinite alternate;
|
|
||||||
background: rgba(var(--v-theme-primary), 0.25);
|
|
||||||
block-size: 300px;
|
|
||||||
filter: blur(55px);
|
|
||||||
inline-size: 300px;
|
|
||||||
inset-block-end: -10%;
|
|
||||||
inset-inline-start: -15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-orb--3 {
|
|
||||||
animation: orb-float-3 10s ease-in-out infinite alternate;
|
|
||||||
background: rgba(var(--v-theme-primary), 0.15);
|
|
||||||
block-size: 220px;
|
|
||||||
filter: blur(50px);
|
|
||||||
inline-size: 220px;
|
|
||||||
inset-block-start: 50%;
|
|
||||||
inset-inline-end: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes orb-float-1 {
|
|
||||||
0% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translate(-30px, 40px) scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes orb-float-2 {
|
|
||||||
0% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translate(25px, -30px) scale(1.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes orb-float-3 {
|
|
||||||
0% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translate(-20px, 20px) scale(0.92);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1181,35 +1041,28 @@ onUnmounted(() => {
|
|||||||
border-radius: var(--app-surface-radius, 20px) !important;
|
border-radius: var(--app-surface-radius, 20px) !important;
|
||||||
box-shadow: 0 20px 54px rgba(var(--app-shadow-rgb, 0, 0, 0), 0.12) !important;
|
box-shadow: 0 20px 54px rgba(var(--app-shadow-rgb, 0, 0, 0), 0.12) !important;
|
||||||
|
|
||||||
> :not(.login-card__glass) {
|
> :not(.login-card__surface) {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 顶部迎光棱镜保持单方向,不随指针另建一套光源。 */
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
border-radius: 999px;
|
background: linear-gradient(
|
||||||
background:
|
|
||||||
radial-gradient(ellipse at center, rgba(255, 255, 255, 0.92), rgba(232, 219, 255, 0.48) 28%, transparent 72%),
|
|
||||||
linear-gradient(
|
|
||||||
90deg,
|
90deg,
|
||||||
transparent,
|
transparent,
|
||||||
rgba(117, 212, 255, 0.16) 30%,
|
rgba(255, 255, 255, 0.42) 32%,
|
||||||
rgba(177, 139, 255, 0.22) 50%,
|
rgba(255, 255, 255, 0.7) 50%,
|
||||||
rgba(255, 105, 210, 0.12) 70%,
|
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
block-size: 1px;
|
block-size: 1px;
|
||||||
content: '';
|
content: '';
|
||||||
filter: drop-shadow(0 1px 3px rgba(var(--v-theme-primary), 0.16));
|
|
||||||
inset-block-start: 0;
|
inset-block-start: 0;
|
||||||
inset-inline-start: clamp(44px, var(--login-card-light-x), calc(100% - 44px));
|
inset-inline: 13% 38%;
|
||||||
inline-size: 88px;
|
opacity: 0.34;
|
||||||
mix-blend-mode: screen;
|
|
||||||
opacity: var(--login-card-top-prism-alpha);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1219,76 +1072,38 @@ onUnmounted(() => {
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card__glass {
|
.login-card__surface {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
backdrop-filter: blur(var(--optical-glass-blur)) saturate(var(--optical-glass-saturate))
|
|
||||||
contrast(var(--optical-glass-contrast));
|
|
||||||
background:
|
|
||||||
radial-gradient(
|
|
||||||
180px 150px at var(--login-card-light-x) var(--login-card-light-y),
|
|
||||||
rgba(255, 255, 255, var(--login-card-highlight-alpha)),
|
|
||||||
rgba(var(--v-theme-primary), var(--login-card-primary-alpha)) 44%,
|
|
||||||
transparent 76%
|
|
||||||
),
|
|
||||||
linear-gradient(145deg, rgba(255, 255, 255, 0.075), transparent 34%), rgba(var(--v-theme-surface), 0.7);
|
|
||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card__glass::before {
|
.login-root[data-login-visual-profile='classic'] .login-card__surface {
|
||||||
position: absolute;
|
backdrop-filter: blur(22px) saturate(118%);
|
||||||
z-index: 2;
|
background: rgba(var(--v-theme-surface), 0.76);
|
||||||
border-radius: 44% 56% 61% 39% / 42% 39% 61% 58%;
|
}
|
||||||
|
|
||||||
|
.login-root[data-login-visual-profile='transparent'] .login-card__surface {
|
||||||
|
backdrop-filter: blur(var(--optical-glass-blur)) saturate(var(--optical-glass-saturate))
|
||||||
|
contrast(var(--optical-glass-contrast));
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 36% 32%, rgba(255, 255, 255, 0.2), transparent 24%),
|
radial-gradient(
|
||||||
conic-gradient(
|
180px 150px at 50% 0%,
|
||||||
from 218deg,
|
rgba(255, 255, 255, var(--login-card-highlight-alpha, 0.035)),
|
||||||
transparent,
|
rgba(var(--v-theme-primary), var(--login-card-primary-alpha, 0.02)) 44%,
|
||||||
rgba(var(--v-theme-primary), 0.13),
|
transparent 76%
|
||||||
transparent 38%,
|
),
|
||||||
rgba(255, 255, 255, 0.09),
|
linear-gradient(145deg, rgba(255, 255, 255, 0.075), transparent 34%), rgba(var(--v-theme-surface), 0.7);
|
||||||
transparent 72%
|
|
||||||
);
|
|
||||||
content: '';
|
|
||||||
filter: blur(var(--optical-glass-pulse-blur));
|
|
||||||
inset: -28%;
|
|
||||||
opacity: var(--optical-glass-glow-opacity);
|
|
||||||
transform: translate(var(--optical-glass-shift-x), var(--optical-glass-shift-y)) rotate(var(--optical-glass-rotation))
|
|
||||||
scale(var(--optical-glass-scale));
|
|
||||||
transition: opacity 220ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card__wallpaper-refraction {
|
.login-root[data-login-visual-profile='glass'] .login-card__surface {
|
||||||
position: absolute;
|
background:
|
||||||
z-index: 0;
|
linear-gradient(145deg, rgba(255, 255, 255, 0.08), transparent 34%),
|
||||||
display: none;
|
linear-gradient(rgba(7, 14, 25, 0.04), rgba(7, 14, 25, 0.1));
|
||||||
inset: -2%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__glass-caustic {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
display: block;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: radial-gradient(
|
|
||||||
ellipse,
|
|
||||||
rgba(255, 255, 255, var(--optical-glass-caustic-highlight-alpha)),
|
|
||||||
rgba(var(--v-theme-primary), var(--optical-glass-caustic-primary-alpha)) 34%,
|
|
||||||
transparent 72%
|
|
||||||
);
|
|
||||||
filter: blur(var(--optical-glass-pulse-blur));
|
|
||||||
inset-block-start: calc(var(--optical-glass-y) - 88px);
|
|
||||||
inset-inline-start: calc(var(--optical-glass-x) - 112px);
|
|
||||||
block-size: 176px;
|
|
||||||
inline-size: 224px;
|
|
||||||
opacity: var(--optical-glass-caustic-opacity);
|
|
||||||
pointer-events: none;
|
|
||||||
transform: scale(var(--optical-glass-caustic-scale));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== 卡片头部 ===================== */
|
/* ===================== 卡片头部 ===================== */
|
||||||
@@ -1500,11 +1315,9 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录按钮:渐变 + 悬浮抬升 + 光泽 */
|
/* 登录按钮使用克制的主题色层级,避免在壁纸上形成独立霓虹光源。 */
|
||||||
.login-submit {
|
.login-submit {
|
||||||
position: relative;
|
box-shadow: 0 6px 18px rgba(var(--v-theme-primary), 0.2);
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 8px 24px rgba(var(--v-theme-primary), 0.35);
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
transition:
|
transition:
|
||||||
@@ -1512,40 +1325,16 @@ onUnmounted(() => {
|
|||||||
box-shadow 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
box-shadow 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 12px 32px rgba(var(--v-theme-primary), 0.45);
|
box-shadow: 0 8px 22px rgba(var(--v-theme-primary), 0.26);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
box-shadow: 0 4px 12px rgba(var(--v-theme-primary), 0.3);
|
box-shadow: 0 3px 10px rgba(var(--v-theme-primary), 0.18);
|
||||||
transform: translateY(0);
|
transform: scale(0.99);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录按钮内部光泽扫描层 */
|
|
||||||
.login-submit :deep(.v-btn__content)::after {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
background: linear-gradient(
|
|
||||||
105deg,
|
|
||||||
transparent 35%,
|
|
||||||
rgba(255, 255, 255, 30%) 43%,
|
|
||||||
rgba(255, 255, 255, 40%) 50%,
|
|
||||||
rgba(255, 255, 255, 30%) 57%,
|
|
||||||
transparent 65%
|
|
||||||
);
|
|
||||||
content: '';
|
|
||||||
inset-block: -50%;
|
|
||||||
inset-inline: -50%;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translateX(-120%);
|
|
||||||
transition: transform 700ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-submit:hover :deep(.v-btn__content)::after {
|
|
||||||
transform: translateX(120%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Passkey 按钮 */
|
/* Passkey 按钮 */
|
||||||
.passkey-btn {
|
.passkey-btn {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -1670,27 +1459,13 @@ onUnmounted(() => {
|
|||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-submit :deep(.v-btn__content)::after {
|
.login-ambient-light__wash {
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-orb {
|
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card__glass::before,
|
|
||||||
.login-card__glass-caustic {
|
|
||||||
transform: none !important;
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-transparency: reduce) {
|
@media (prefers-reduced-transparency: reduce) {
|
||||||
.login-card__wallpaper-refraction {
|
.login-card__surface,
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__glass,
|
|
||||||
.native-login-field {
|
.native-login-field {
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
background: rgb(var(--v-theme-surface)) !important;
|
background: rgb(var(--v-theme-surface)) !important;
|
||||||
@@ -1698,7 +1473,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
@media (prefers-contrast: more) {
|
||||||
.login-card__glass {
|
.login-card__surface {
|
||||||
background: rgba(var(--v-theme-surface), 0.94);
|
background: rgba(var(--v-theme-surface), 0.94);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1708,11 +1483,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@supports not (backdrop-filter: blur(1px)) {
|
@supports not (backdrop-filter: blur(1px)) {
|
||||||
.login-card__wallpaper-refraction {
|
.login-card__surface,
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__glass,
|
|
||||||
.native-login-field {
|
.native-login-field {
|
||||||
background: rgba(var(--v-theme-surface), 0.96) !important;
|
background: rgba(var(--v-theme-surface), 0.96) !important;
|
||||||
}
|
}
|
||||||
@@ -1732,20 +1503,6 @@ onUnmounted(() => {
|
|||||||
padding: 1.5rem !important;
|
padding: 1.5rem !important;
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-orb--1 {
|
|
||||||
block-size: 220px;
|
|
||||||
inline-size: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-orb--2 {
|
|
||||||
block-size: 180px;
|
|
||||||
inline-size: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-orb--3 {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 480px) and (height <= 600px) {
|
@media (width <= 480px) and (height <= 600px) {
|
||||||
|
|||||||
+31
-101
@@ -14,6 +14,7 @@ html[data-theme='glass'] {
|
|||||||
--glass-border-hover: rgba(255, 255, 255, calc(0.1 + var(--glass-reflection, 0.5) * 0.2));
|
--glass-border-hover: rgba(255, 255, 255, calc(0.1 + var(--glass-reflection, 0.5) * 0.2));
|
||||||
--glass-control-prominent-border: rgba(255, 255, 255, 14%);
|
--glass-control-prominent-border: rgba(255, 255, 255, 14%);
|
||||||
--glass-highlight: rgba(255, 255, 255, calc(0.06 + var(--glass-reflection, 0.5) * 0.16));
|
--glass-highlight: rgba(255, 255, 255, calc(0.06 + var(--glass-reflection, 0.5) * 0.16));
|
||||||
|
--glass-transmission-brightness: 1;
|
||||||
--glass-sheen: linear-gradient(
|
--glass-sheen: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(255, 255, 255, 9%),
|
rgba(255, 255, 255, 9%),
|
||||||
@@ -41,8 +42,8 @@ html[data-theme='glass'] {
|
|||||||
--glass-control-prominent-focus-shadow:
|
--glass-control-prominent-focus-shadow:
|
||||||
0 0 0 2px rgba(var(--v-theme-primary), 16%), inset 0 1px 0 rgba(255, 255, 255, 24%),
|
0 0 0 2px rgba(var(--v-theme-primary), 16%), inset 0 1px 0 rgba(255, 255, 255, 24%),
|
||||||
inset 0 -1px 0 rgba(2, 6, 16, 16%);
|
inset 0 -1px 0 rgba(2, 6, 16, 16%);
|
||||||
--glass-surface-backdrop-filter: none;
|
--glass-surface-backdrop-filter: brightness(var(--glass-transmission-brightness));
|
||||||
--glass-raised-backdrop-filter: none;
|
--glass-raised-backdrop-filter: brightness(var(--glass-transmission-brightness));
|
||||||
--glass-control-backdrop-filter: none;
|
--glass-control-backdrop-filter: none;
|
||||||
--glass-control-prominent-backdrop-filter: none;
|
--glass-control-prominent-backdrop-filter: none;
|
||||||
--glass-navbar-backdrop-filter: var(--glass-raised-backdrop-filter);
|
--glass-navbar-backdrop-filter: var(--glass-raised-backdrop-filter);
|
||||||
@@ -189,8 +190,10 @@ html[data-theme='glass'] {
|
|||||||
--glass-control-prominent-focus-shadow:
|
--glass-control-prominent-focus-shadow:
|
||||||
0 0 0 2px rgba(var(--v-theme-primary), 17%), inset 0 1px 0 rgba(255, 255, 255, 34%),
|
0 0 0 2px rgba(var(--v-theme-primary), 17%), inset 0 1px 0 rgba(255, 255, 255, 34%),
|
||||||
inset 0 -1px 0 rgba(2, 6, 16, 18%);
|
inset 0 -1px 0 rgba(2, 6, 16, 18%);
|
||||||
--glass-surface-backdrop-filter: blur(var(--glass-blur-surface)) saturate(var(--glass-saturate));
|
--glass-surface-backdrop-filter: blur(var(--glass-blur-surface)) saturate(var(--glass-saturate))
|
||||||
--glass-raised-backdrop-filter: blur(var(--glass-blur-raised)) saturate(var(--glass-saturate));
|
brightness(var(--glass-transmission-brightness));
|
||||||
|
--glass-raised-backdrop-filter: blur(var(--glass-blur-raised)) saturate(var(--glass-saturate))
|
||||||
|
brightness(var(--glass-transmission-brightness));
|
||||||
--glass-control-prominent-backdrop-filter: blur(24px) saturate(150%);
|
--glass-control-prominent-backdrop-filter: blur(24px) saturate(150%);
|
||||||
--glass-overlay-surface: var(--glass-surface-raised);
|
--glass-overlay-surface: var(--glass-surface-raised);
|
||||||
--glass-overlay-blur: var(--glass-blur-raised);
|
--glass-overlay-blur: var(--glass-blur-raised);
|
||||||
@@ -204,7 +207,8 @@ html[data-theme='glass'] {
|
|||||||
--glass-control-shortcut-color: rgba(242, 245, 250, 76%);
|
--glass-control-shortcut-color: rgba(242, 245, 250, 76%);
|
||||||
--glass-button-surface: rgba(255, 255, 255, 10%);
|
--glass-button-surface: rgba(255, 255, 255, 10%);
|
||||||
--glass-button-surface-hover: rgba(255, 255, 255, 14%);
|
--glass-button-surface-hover: rgba(255, 255, 255, 14%);
|
||||||
--glass-dashboard-backdrop-filter: blur(var(--glass-blur-surface)) saturate(var(--glass-saturate));
|
--glass-dashboard-backdrop-filter: blur(var(--glass-blur-surface)) saturate(var(--glass-saturate))
|
||||||
|
brightness(var(--glass-transmission-brightness));
|
||||||
|
|
||||||
// 磨砂弹窗只由最外层表面采样背景,内部卡片与自定义分组表面保持扁平。
|
// 磨砂弹窗只由最外层表面采样背景,内部卡片与自定义分组表面保持扁平。
|
||||||
.v-overlay__content > :where(.v-card, .v-sheet),
|
.v-overlay__content > :where(.v-card, .v-sheet),
|
||||||
@@ -1011,16 +1015,16 @@ html[data-theme='glass'] {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录卡片的壁纸副本与外层背景共享切换时序,避免卡片内部提前显示下一张图片。
|
|
||||||
html[data-theme='glass'] .app-wrapper--background-transition .login-card__wallpaper-refraction {
|
|
||||||
opacity: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html:is([data-glass-quality='balanced'], [data-glass-quality='high'])[data-glass-renderer-state='ready']
|
html:is([data-glass-quality='balanced'], [data-glass-quality='high'])[data-glass-renderer-state='ready']
|
||||||
.glass-optical-layer {
|
.glass-optical-layer {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 登录页强制使用高质量能力,但不改写用户保存的全局质量属性。
|
||||||
|
html[data-glass-renderer-state='ready'] .app-wrapper--login-glass-high .glass-optical-layer {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
// 光学档位下顶栏滚动前后共用同一 filter,避免滚动态重新叠加高强度模糊。
|
// 光学档位下顶栏滚动前后共用同一 filter,避免滚动态重新叠加高强度模糊。
|
||||||
html[data-theme='glass']:is(
|
html[data-theme='glass']:is(
|
||||||
[data-glass-quality='balanced'],
|
[data-glass-quality='balanced'],
|
||||||
@@ -1079,54 +1083,21 @@ html[data-glass-appearance='frosted']:is(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录页只有一个高价值表面,三档质量共用局部实时材质,避免整屏 renderer 重复采样跨域壁纸。
|
// 登录页只保留内容保护表面;折射、方向反射和唯一动态焦散均由共享 renderer 负责。
|
||||||
html[data-theme='glass'] body[data-theme='glass'] {
|
html[data-theme='glass'] body[data-theme='glass'] {
|
||||||
.login-card {
|
.login-card {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.16),
|
inset 0 0 0 1px rgba(255, 255, 255, 0.14),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||||
0 24px 64px rgba(1, 6, 15, 0.22) !important;
|
0 24px 64px rgba(1, 6, 15, 0.22) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card__glass {
|
.login-card__surface {
|
||||||
-webkit-backdrop-filter: var(--glass-surface-backdrop-filter) !important;
|
-webkit-backdrop-filter: brightness(var(--glass-transmission-brightness));
|
||||||
backdrop-filter: var(--glass-surface-backdrop-filter) !important;
|
backdrop-filter: brightness(var(--glass-transmission-brightness));
|
||||||
background:
|
background:
|
||||||
radial-gradient(
|
linear-gradient(145deg, rgba(255, 255, 255, 0.08), transparent 34%),
|
||||||
220px 190px at var(--login-card-light-x) var(--login-card-light-y),
|
linear-gradient(rgba(7, 14, 25, 0.04), rgba(7, 14, 25, 0.1)) !important;
|
||||||
rgba(255, 255, 255, 0.13),
|
|
||||||
rgba(var(--v-theme-primary), 0.045) 42%,
|
|
||||||
transparent 76%
|
|
||||||
),
|
|
||||||
linear-gradient(145deg, rgba(255, 255, 255, 0.1), transparent 36%), rgba(var(--v-theme-surface), 0.28) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__wallpaper-refraction {
|
|
||||||
display: block;
|
|
||||||
background-attachment: fixed;
|
|
||||||
background-image: var(--login-wallpaper-image);
|
|
||||||
background-position: calc(50% + var(--login-refraction-shift-x)) calc(50% + var(--login-refraction-shift-y));
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
filter: url('#login-liquid-refraction-clear') saturate(1.22) contrast(1.06) brightness(1.04);
|
|
||||||
opacity: 0.48;
|
|
||||||
transform: scale(1.025);
|
|
||||||
transition:
|
|
||||||
background-position 90ms linear,
|
|
||||||
opacity 180ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__glass-caustic {
|
|
||||||
inset-block-start: calc(var(--login-card-light-y) - 88px);
|
|
||||||
inset-inline-start: calc(var(--login-card-light-x) - 112px);
|
|
||||||
mix-blend-mode: screen;
|
|
||||||
opacity: calc(var(--optical-glass-caustic-opacity) + 0.12);
|
|
||||||
transform: scale(calc(var(--optical-glass-caustic-scale) + 0.08));
|
|
||||||
transition:
|
|
||||||
inset-block-start 90ms linear,
|
|
||||||
inset-inline-start 90ms linear,
|
|
||||||
opacity 160ms ease-out,
|
|
||||||
transform 180ms ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.native-login-field {
|
.native-login-field {
|
||||||
@@ -1138,22 +1109,11 @@ html[data-theme='glass'] body[data-theme='glass'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='glass'][data-glass-appearance='tinted'] body[data-theme='glass'] {
|
html[data-theme='glass'][data-glass-appearance='tinted'] body[data-theme='glass'] {
|
||||||
.login-card__glass {
|
.login-card__surface {
|
||||||
background:
|
background:
|
||||||
radial-gradient(
|
|
||||||
220px 190px at var(--login-card-light-x) var(--login-card-light-y),
|
|
||||||
rgba(255, 255, 255, 0.13),
|
|
||||||
rgba(var(--v-theme-primary), 0.1) 44%,
|
|
||||||
transparent 76%
|
|
||||||
),
|
|
||||||
linear-gradient(145deg, rgba(255, 255, 255, 0.1), transparent 36%),
|
linear-gradient(145deg, rgba(255, 255, 255, 0.1), transparent 36%),
|
||||||
linear-gradient(rgba(var(--v-theme-primary), 0.1), rgba(var(--v-theme-primary), 0.04)),
|
linear-gradient(rgba(var(--v-theme-primary), 0.08), rgba(var(--v-theme-primary), 0.03)),
|
||||||
rgba(var(--v-theme-surface), 0.28) !important;
|
linear-gradient(rgba(7, 14, 25, 0.16), rgba(7, 14, 25, 0.3)) !important;
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__wallpaper-refraction {
|
|
||||||
filter: url('#login-liquid-refraction-clear') saturate(1.22) contrast(1.06) brightness(1.04);
|
|
||||||
opacity: 0.48;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.native-login-field {
|
.native-login-field {
|
||||||
@@ -1168,42 +1128,12 @@ html[data-theme='glass'][data-glass-appearance='frosted'] body[data-theme='glass
|
|||||||
box-shadow: var(--glass-shadow-raised) !important;
|
box-shadow: var(--glass-shadow-raised) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card__glass {
|
.login-card__surface {
|
||||||
-webkit-backdrop-filter: var(--glass-raised-backdrop-filter) !important;
|
-webkit-backdrop-filter: blur(var(--glass-blur-raised)) saturate(var(--glass-saturate))
|
||||||
backdrop-filter: var(--glass-raised-backdrop-filter) !important;
|
brightness(var(--glass-transmission-brightness));
|
||||||
background-color: var(--glass-surface-raised) !important;
|
backdrop-filter: blur(var(--glass-blur-raised)) saturate(var(--glass-saturate))
|
||||||
background-image: var(--glass-sheen) !important;
|
brightness(var(--glass-transmission-brightness));
|
||||||
}
|
background: var(--glass-sheen), linear-gradient(rgba(8, 15, 27, 0.34), rgba(8, 15, 27, 0.48)) !important;
|
||||||
|
|
||||||
.login-card__glass::after {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
|
||||||
background-image: var(--glass-sheen);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.22),
|
|
||||||
inset 0 -1px 0 rgba(2, 6, 16, 0.16);
|
|
||||||
content: '';
|
|
||||||
inset: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录页位于独立合成边界,壁纸副本用于恢复导航表面的背景采样语义。
|
|
||||||
.login-card__wallpaper-refraction {
|
|
||||||
display: block;
|
|
||||||
background-image: linear-gradient(rgba(6, 10, 19, 0.3), rgba(6, 10, 19, 0.58)), var(--login-wallpaper-image);
|
|
||||||
background-attachment: fixed, fixed;
|
|
||||||
background-position:
|
|
||||||
center,
|
|
||||||
calc(50% + var(--login-refraction-shift-x)) calc(50% + var(--login-refraction-shift-y));
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
filter: blur(52px) brightness(0.76) saturate(0.72);
|
|
||||||
opacity: 0.94;
|
|
||||||
transform: scale(1.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card__glass-caustic {
|
|
||||||
opacity: calc(var(--optical-glass-caustic-opacity) + 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.native-login-field {
|
.native-login-field {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { commitPreloadedBackgroundRotation, preloadBackgroundRotationImages } from '@/utils/backgroundRotation'
|
import {
|
||||||
|
BACKGROUND_ROTATION_GRACE_MS,
|
||||||
|
commitPreloadedBackgroundRotation,
|
||||||
|
preloadBackgroundRotationImages,
|
||||||
|
preloadBackgroundSequence,
|
||||||
|
shouldAllowBackgroundRotation,
|
||||||
|
} from '@/utils/backgroundRotation'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
function deferred<T>() {
|
function deferred<T>() {
|
||||||
@@ -10,6 +16,18 @@ function deferred<T>() {
|
|||||||
return { promise, resolve }
|
return { promise, resolve }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('background rotation lifecycle', () => {
|
||||||
|
it('keeps wallpaper rotation independent from paused interaction effects during the bounded grace period', () => {
|
||||||
|
expect(BACKGROUND_ROTATION_GRACE_MS).toBe(60_000)
|
||||||
|
expect(shouldAllowBackgroundRotation('active', false, false)).toBe(true)
|
||||||
|
expect(shouldAllowBackgroundRotation('passive', true, false)).toBe(true)
|
||||||
|
expect(shouldAllowBackgroundRotation('suspended', true, false)).toBe(true)
|
||||||
|
expect(shouldAllowBackgroundRotation('passive', false, false)).toBe(false)
|
||||||
|
expect(shouldAllowBackgroundRotation('idle', false, false)).toBe(false)
|
||||||
|
expect(shouldAllowBackgroundRotation('active', false, true)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('commitPreloadedBackgroundRotation', () => {
|
describe('commitPreloadedBackgroundRotation', () => {
|
||||||
it('drops a successful preload when the rotation becomes inactive before completion', async () => {
|
it('drops a successful preload when the rotation becomes inactive before completion', async () => {
|
||||||
const preload = deferred<boolean>()
|
const preload = deferred<boolean>()
|
||||||
@@ -87,3 +105,38 @@ describe('preloadBackgroundRotationImages', () => {
|
|||||||
expect(preload).toHaveBeenCalledTimes(2)
|
expect(preload).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('preloadBackgroundSequence', () => {
|
||||||
|
it('preloads remaining wallpapers sequentially in rotation order', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
preloadBackgroundSequence({
|
||||||
|
canContinue: () => true,
|
||||||
|
preload: async url => {
|
||||||
|
calls.push(url)
|
||||||
|
return url !== 'two.jpg'
|
||||||
|
},
|
||||||
|
urls: ['one.jpg', 'two.jpg', 'three.jpg'],
|
||||||
|
}),
|
||||||
|
).resolves.toEqual([true, false, true])
|
||||||
|
expect(calls).toEqual(['one.jpg', 'two.jpg', 'three.jpg'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stops an obsolete queue before starting the next image', async () => {
|
||||||
|
let active = true
|
||||||
|
const calls: string[] = []
|
||||||
|
|
||||||
|
await preloadBackgroundSequence({
|
||||||
|
canContinue: () => active,
|
||||||
|
preload: async url => {
|
||||||
|
calls.push(url)
|
||||||
|
active = false
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
urls: ['one.jpg', 'two.jpg'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls).toEqual(['one.jpg'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
GLASS_OPTICAL_REFLECTION_MAX_SCALE,
|
GLASS_OPTICAL_REFLECTION_MAX_SCALE,
|
||||||
getAvailableGlassOpticalPresets,
|
getAvailableGlassOpticalPresets,
|
||||||
getGlassCoverScale,
|
getGlassCoverScale,
|
||||||
|
getGlassOpticalCssTransmissionBrightness,
|
||||||
getGlassOpticalDecay,
|
getGlassOpticalDecay,
|
||||||
getGlassOpticalBufferSize,
|
getGlassOpticalBufferSize,
|
||||||
getGlassOpticalMaxRefractionPixels,
|
getGlassOpticalMaxRefractionPixels,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
getGlassOpticalReflectionStrengthScale,
|
getGlassOpticalReflectionStrengthScale,
|
||||||
getGlassOpticalRenderProfile,
|
getGlassOpticalRenderProfile,
|
||||||
getGlassOpticalTransparency,
|
getGlassOpticalTransparency,
|
||||||
|
getGlassOpticalTransmissionStrength,
|
||||||
getGlassOpticalSurfaceTransitionWeights,
|
getGlassOpticalSurfaceTransitionWeights,
|
||||||
getGlassOpticalWakeDirection,
|
getGlassOpticalWakeDirection,
|
||||||
getGlassOpticalWakeSample,
|
getGlassOpticalWakeSample,
|
||||||
@@ -66,14 +68,20 @@ describe('glass optics geometry', () => {
|
|||||||
expect(getGlassOpticalReflectionStrengthScale(100)).toBeCloseTo(GLASS_OPTICAL_REFLECTION_MAX_SCALE)
|
expect(getGlassOpticalReflectionStrengthScale(100)).toBeCloseTo(GLASS_OPTICAL_REFLECTION_MAX_SCALE)
|
||||||
expect(getGlassOpticalTransparency(0)).toBeCloseTo(0.28)
|
expect(getGlassOpticalTransparency(0)).toBeCloseTo(0.28)
|
||||||
expect(getGlassOpticalTransparency(50)).toBeGreaterThan(0.6)
|
expect(getGlassOpticalTransparency(50)).toBeGreaterThan(0.6)
|
||||||
expect(getGlassOpticalTransparency(80)).toBeCloseTo(0.86)
|
expect(getGlassOpticalTransparency(70)).toBeCloseTo(0.96)
|
||||||
expect(getGlassOpticalTransparency(100)).toBeCloseTo(0.96)
|
expect(getGlassOpticalTransparency(100)).toBeCloseTo(1.1)
|
||||||
expect(getGlassOpticalTransparency(100) - getGlassOpticalTransparency(80)).toBeLessThan(
|
expect(getGlassOpticalTransparency(100) - getGlassOpticalTransparency(70)).toBeLessThan(
|
||||||
getGlassOpticalTransparency(80) - getGlassOpticalTransparency(50),
|
getGlassOpticalTransparency(70) - getGlassOpticalTransparency(50),
|
||||||
)
|
)
|
||||||
|
expect(getGlassOpticalTransmissionStrength(0)).toBe(0)
|
||||||
|
expect(getGlassOpticalTransmissionStrength(70)).toBe(1)
|
||||||
|
expect(getGlassOpticalTransmissionStrength(100)).toBe(1.3)
|
||||||
|
expect(getGlassOpticalCssTransmissionBrightness(0)).toBeCloseTo(0.82)
|
||||||
|
expect(getGlassOpticalCssTransmissionBrightness(70)).toBeCloseTo(1.28)
|
||||||
|
expect(getGlassOpticalCssTransmissionBrightness(100)).toBeCloseTo(1.48)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps presets as concrete five-parameter values', () => {
|
it('keeps presets as concrete six-parameter values', () => {
|
||||||
const natural = getGlassOpticalPresetParameters('clear', 'balanced', 'natural')
|
const natural = getGlassOpticalPresetParameters('clear', 'balanced', 'natural')
|
||||||
const glide = getGlassOpticalPresetParameters('clear', 'balanced', 'glide')
|
const glide = getGlassOpticalPresetParameters('clear', 'balanced', 'glide')
|
||||||
const liquid = getGlassOpticalPresetParameters('frosted', 'high', 'liquid')
|
const liquid = getGlassOpticalPresetParameters('frosted', 'high', 'liquid')
|
||||||
@@ -81,9 +89,10 @@ describe('glass optics geometry', () => {
|
|||||||
expect(natural).toEqual({
|
expect(natural).toEqual({
|
||||||
deformation: 50,
|
deformation: 50,
|
||||||
flow: 50,
|
flow: 50,
|
||||||
reflection: 50,
|
reflection: 35,
|
||||||
|
transmission: 70,
|
||||||
translation: 50,
|
translation: 50,
|
||||||
transparency: 50,
|
transparency: 70,
|
||||||
})
|
})
|
||||||
expect(glide.translation).toBeGreaterThan(glide.deformation)
|
expect(glide.translation).toBeGreaterThan(glide.deformation)
|
||||||
expect(liquid.deformation).toBeGreaterThan(glide.deformation)
|
expect(liquid.deformation).toBeGreaterThan(glide.deformation)
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
activateLoginBackgroundLayer,
|
||||||
|
createLoginBackgroundLayers,
|
||||||
|
getLoginGlassOpticalSettings,
|
||||||
|
getLoginVisualProfile,
|
||||||
|
getLoginWallpaperRequestMode,
|
||||||
|
prepareLoginBackgroundLayer,
|
||||||
|
settleLoginBackgroundLayers,
|
||||||
|
} from '@/utils/loginPresentation'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('login presentation', () => {
|
||||||
|
it('maps resolved themes to mutually exclusive visual profiles', () => {
|
||||||
|
expect(getLoginVisualProfile('glass')).toBe('glass')
|
||||||
|
expect(getLoginVisualProfile('transparent')).toBe('transparent')
|
||||||
|
expect(getLoginVisualProfile('purple')).toBe('classic')
|
||||||
|
expect(getLoginVisualProfile('light')).toBe('classic')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forces high-quality capability while preserving all six user strengths', () => {
|
||||||
|
expect(
|
||||||
|
getLoginGlassOpticalSettings({
|
||||||
|
appearance: 'frosted',
|
||||||
|
deformationStrength: 73,
|
||||||
|
flowStrength: 68,
|
||||||
|
preset: 'liquid',
|
||||||
|
reflectionStrength: 41,
|
||||||
|
transmissionStrength: 62,
|
||||||
|
translationStrength: 57,
|
||||||
|
transparencyStrength: 46,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
appearance: 'frosted',
|
||||||
|
deformationStrength: 73,
|
||||||
|
flowStrength: 68,
|
||||||
|
preset: 'liquid',
|
||||||
|
quality: 'high',
|
||||||
|
reflectionStrength: 41,
|
||||||
|
transmissionStrength: 62,
|
||||||
|
translationStrength: 57,
|
||||||
|
transparencyStrength: 46,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requests same-origin wallpapers only for glass', () => {
|
||||||
|
expect(getLoginWallpaperRequestMode('glass')).toBe('same-origin')
|
||||||
|
expect(getLoginWallpaperRequestMode('classic')).toBe('default')
|
||||||
|
expect(getLoginWallpaperRequestMode('transparent')).toBe('default')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps two stable wallpaper slots while their transition roles change', () => {
|
||||||
|
const initial = createLoginBackgroundLayers('one.jpg')
|
||||||
|
const prepared = prepareLoginBackgroundLayer(initial, 'two.jpg')
|
||||||
|
const activated = activateLoginBackgroundLayer(prepared)
|
||||||
|
const settled = settleLoginBackgroundLayers(activated)
|
||||||
|
|
||||||
|
expect(initial).toEqual([
|
||||||
|
{ key: 'front', role: 'active', url: 'one.jpg' },
|
||||||
|
{ key: 'back', role: 'standby', url: '' },
|
||||||
|
])
|
||||||
|
expect(prepared).toEqual([
|
||||||
|
{ key: 'front', role: 'active', url: 'one.jpg' },
|
||||||
|
{ key: 'back', role: 'standby', url: 'two.jpg' },
|
||||||
|
])
|
||||||
|
expect(activated).toEqual([
|
||||||
|
{ key: 'front', role: 'previous', url: 'one.jpg' },
|
||||||
|
{ key: 'back', role: 'active', url: 'two.jpg' },
|
||||||
|
])
|
||||||
|
expect(settled).toEqual([
|
||||||
|
{ key: 'front', role: 'standby', url: '' },
|
||||||
|
{ key: 'back', role: 'active', url: 'two.jpg' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
|
import type { AppActivityState } from '@/utils/appActivityLifecycle'
|
||||||
|
|
||||||
|
/** 壁纸在窗口失焦后继续轮换的最长时间,交互 renderer 仍由应用生命周期独立暂停。 */
|
||||||
|
export const BACKGROUND_ROTATION_GRACE_MS = 60_000
|
||||||
|
|
||||||
|
/** 壁纸轮换只在前台活动或失焦宽限期内运行,系统减少动态效果时始终停止。 */
|
||||||
|
export function shouldAllowBackgroundRotation(state: AppActivityState, graceActive: boolean, reducedMotion: boolean) {
|
||||||
|
return !reducedMotion && (state === 'active' || graceActive)
|
||||||
|
}
|
||||||
|
|
||||||
interface PreloadedBackgroundRotationOptions {
|
interface PreloadedBackgroundRotationOptions {
|
||||||
/** 提交前重新判断当前生命周期和请求版本是否仍允许切换。 */
|
/** 提交前重新判断当前生命周期和请求版本是否仍允许切换。 */
|
||||||
canCommit: () => boolean
|
canCommit: () => boolean
|
||||||
@@ -16,6 +26,15 @@ interface BackgroundRotationImagePreloadOptions {
|
|||||||
preload: (url: string) => Promise<boolean>
|
preload: (url: string) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BackgroundSequencePreloadOptions {
|
||||||
|
/** 每张图片完成后重新判断队列是否仍属于当前页面与请求代次。 */
|
||||||
|
canContinue: () => boolean
|
||||||
|
/** 按实际轮播顺序排列的待预加载地址。 */
|
||||||
|
urls: string[]
|
||||||
|
/** 执行单张图片预加载并返回可用状态。 */
|
||||||
|
preload: (url: string) => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将壁纸预加载与最终提交分离,确保异步加载期间失效的轮换请求不会改变可见背景。
|
* 将壁纸预加载与最终提交分离,确保异步加载期间失效的轮换请求不会改变可见背景。
|
||||||
*/
|
*/
|
||||||
@@ -40,3 +59,15 @@ export async function preloadBackgroundRotationImages(options: BackgroundRotatio
|
|||||||
|
|
||||||
return results.every(Boolean)
|
return results.every(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 当前壁纸稳定后串行预加载剩余轮播项,避免并发争抢首屏带宽。 */
|
||||||
|
export async function preloadBackgroundSequence(options: BackgroundSequencePreloadOptions) {
|
||||||
|
const results: boolean[] = []
|
||||||
|
|
||||||
|
for (const url of options.urls) {
|
||||||
|
if (!options.canContinue()) break
|
||||||
|
results.push(await options.preload(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|||||||
+63
-34
@@ -8,6 +8,7 @@ export const GLASS_OPTICAL_TRANSLATION_MAX_SCALE = 1.7
|
|||||||
export const GLASS_OPTICAL_STRENGTH_DEFAULT = 50
|
export const GLASS_OPTICAL_STRENGTH_DEFAULT = 50
|
||||||
export const GLASS_OPTICAL_STRENGTH_MAX = 100
|
export const GLASS_OPTICAL_STRENGTH_MAX = 100
|
||||||
export const GLASS_OPTICAL_STRENGTH_MIN = 0
|
export const GLASS_OPTICAL_STRENGTH_MIN = 0
|
||||||
|
export const GLASS_OPTICAL_REFERENCE_STRENGTH = 70
|
||||||
|
|
||||||
export type GlassAppearance = 'clear' | 'frosted' | 'tinted'
|
export type GlassAppearance = 'clear' | 'frosted' | 'tinted'
|
||||||
export type GlassOpticalCapability = 'balanced' | 'css' | 'high'
|
export type GlassOpticalCapability = 'balanced' | 'css' | 'high'
|
||||||
@@ -22,6 +23,8 @@ export interface GlassOpticalParameters {
|
|||||||
flow: number
|
flow: number
|
||||||
/** 方向高光、迎光棱镜与背光吸收强度。 */
|
/** 方向高光、迎光棱镜与背光吸收强度。 */
|
||||||
reflection: number
|
reflection: number
|
||||||
|
/** 玻璃内部壁纸采样的明暗与暗部展开强度。 */
|
||||||
|
transmission: number
|
||||||
/** 共享壁纸采样在表面内的统一坐标平移强度。 */
|
/** 共享壁纸采样在表面内的统一坐标平移强度。 */
|
||||||
translation: number
|
translation: number
|
||||||
/** 壁纸可见度与材质遮罩强度。 */
|
/** 壁纸可见度与材质遮罩强度。 */
|
||||||
@@ -127,58 +130,58 @@ const GLASS_OPTICAL_PRESET_MATRIX: Record<
|
|||||||
> = {
|
> = {
|
||||||
clear: {
|
clear: {
|
||||||
css: {
|
css: {
|
||||||
natural: { deformation: 50, flow: 50, reflection: 50, translation: 50, transparency: 50 },
|
natural: { deformation: 50, flow: 50, reflection: 35, transmission: 70, translation: 50, transparency: 70 },
|
||||||
glide: { deformation: 28, flow: 42, reflection: 42, translation: 72, transparency: 58 },
|
glide: { deformation: 28, flow: 42, reflection: 29, transmission: 72, translation: 72, transparency: 78 },
|
||||||
liquid: { deformation: 72, flow: 78, reflection: 54, translation: 56, transparency: 52 },
|
liquid: { deformation: 72, flow: 78, reflection: 38, transmission: 66, translation: 56, transparency: 74 },
|
||||||
},
|
},
|
||||||
balanced: {
|
balanced: {
|
||||||
natural: { deformation: 50, flow: 50, reflection: 50, translation: 50, transparency: 50 },
|
natural: { deformation: 50, flow: 50, reflection: 35, transmission: 70, translation: 50, transparency: 70 },
|
||||||
glide: { deformation: 30, flow: 44, reflection: 42, translation: 72, transparency: 58 },
|
glide: { deformation: 30, flow: 44, reflection: 29, transmission: 72, translation: 72, transparency: 78 },
|
||||||
liquid: { deformation: 70, flow: 76, reflection: 52, translation: 56, transparency: 52 },
|
liquid: { deformation: 70, flow: 76, reflection: 36, transmission: 66, translation: 56, transparency: 74 },
|
||||||
},
|
},
|
||||||
high: {
|
high: {
|
||||||
natural: { deformation: 50, flow: 50, reflection: 46, translation: 50, transparency: 50 },
|
natural: { deformation: 50, flow: 50, reflection: 32, transmission: 70, translation: 50, transparency: 70 },
|
||||||
glide: { deformation: 32, flow: 46, reflection: 40, translation: 74, transparency: 60 },
|
glide: { deformation: 32, flow: 46, reflection: 28, transmission: 74, translation: 74, transparency: 80 },
|
||||||
liquid: { deformation: 74, flow: 80, reflection: 50, translation: 58, transparency: 54 },
|
liquid: { deformation: 74, flow: 80, reflection: 35, transmission: 68, translation: 58, transparency: 76 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tinted: {
|
tinted: {
|
||||||
css: {
|
css: {
|
||||||
natural: { deformation: 50, flow: 50, reflection: 54, translation: 50, transparency: 46 },
|
natural: { deformation: 50, flow: 50, reflection: 38, transmission: 68, translation: 50, transparency: 46 },
|
||||||
glide: { deformation: 30, flow: 42, reflection: 48, translation: 70, transparency: 52 },
|
glide: { deformation: 30, flow: 42, reflection: 34, transmission: 74, translation: 70, transparency: 52 },
|
||||||
liquid: { deformation: 70, flow: 76, reflection: 58, translation: 54, transparency: 48 },
|
liquid: { deformation: 70, flow: 76, reflection: 41, transmission: 64, translation: 54, transparency: 48 },
|
||||||
},
|
},
|
||||||
balanced: {
|
balanced: {
|
||||||
natural: { deformation: 52, flow: 50, reflection: 54, translation: 50, transparency: 46 },
|
natural: { deformation: 52, flow: 50, reflection: 38, transmission: 72, translation: 50, transparency: 46 },
|
||||||
glide: { deformation: 32, flow: 44, reflection: 48, translation: 70, transparency: 52 },
|
glide: { deformation: 32, flow: 44, reflection: 34, transmission: 78, translation: 70, transparency: 52 },
|
||||||
liquid: { deformation: 72, flow: 76, reflection: 56, translation: 56, transparency: 48 },
|
liquid: { deformation: 72, flow: 76, reflection: 39, transmission: 68, translation: 56, transparency: 48 },
|
||||||
},
|
},
|
||||||
high: {
|
high: {
|
||||||
natural: { deformation: 52, flow: 50, reflection: 50, translation: 50, transparency: 48 },
|
natural: { deformation: 52, flow: 50, reflection: 35, transmission: 76, translation: 50, transparency: 48 },
|
||||||
glide: { deformation: 34, flow: 46, reflection: 46, translation: 72, transparency: 54 },
|
glide: { deformation: 34, flow: 46, reflection: 32, transmission: 82, translation: 72, transparency: 54 },
|
||||||
liquid: { deformation: 76, flow: 80, reflection: 54, translation: 58, transparency: 50 },
|
liquid: { deformation: 76, flow: 80, reflection: 38, transmission: 72, translation: 58, transparency: 50 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
frosted: {
|
frosted: {
|
||||||
css: {
|
css: {
|
||||||
natural: { deformation: 50, flow: 50, reflection: 44, translation: 50, transparency: 42 },
|
natural: { deformation: 50, flow: 50, reflection: 31, transmission: 54, translation: 50, transparency: 42 },
|
||||||
glide: { deformation: 34, flow: 42, reflection: 38, translation: 66, transparency: 46 },
|
glide: { deformation: 34, flow: 42, reflection: 27, transmission: 58, translation: 66, transparency: 46 },
|
||||||
liquid: { deformation: 76, flow: 74, reflection: 48, translation: 50, transparency: 44 },
|
liquid: { deformation: 76, flow: 74, reflection: 34, transmission: 50, translation: 50, transparency: 44 },
|
||||||
},
|
},
|
||||||
balanced: {
|
balanced: {
|
||||||
natural: { deformation: 58, flow: 52, reflection: 44, translation: 48, transparency: 42 },
|
natural: { deformation: 58, flow: 52, reflection: 31, transmission: 58, translation: 48, transparency: 42 },
|
||||||
glide: { deformation: 38, flow: 44, reflection: 38, translation: 68, transparency: 46 },
|
glide: { deformation: 38, flow: 44, reflection: 27, transmission: 62, translation: 68, transparency: 46 },
|
||||||
liquid: { deformation: 78, flow: 76, reflection: 46, translation: 52, transparency: 44 },
|
liquid: { deformation: 78, flow: 76, reflection: 32, transmission: 54, translation: 52, transparency: 44 },
|
||||||
},
|
},
|
||||||
high: {
|
high: {
|
||||||
natural: { deformation: 60, flow: 52, reflection: 42, translation: 48, transparency: 44 },
|
natural: { deformation: 60, flow: 52, reflection: 29, transmission: 62, translation: 48, transparency: 44 },
|
||||||
glide: { deformation: 40, flow: 46, reflection: 36, translation: 70, transparency: 48 },
|
glide: { deformation: 40, flow: 46, reflection: 25, transmission: 66, translation: 70, transparency: 48 },
|
||||||
liquid: { deformation: 82, flow: 80, reflection: 44, translation: 54, transparency: 46 },
|
liquid: { deformation: 82, flow: 80, reflection: 31, transmission: 58, translation: 54, transparency: 46 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 返回材质、质量与预置共同确定的五个具体参数,调用方可以安全修改返回值。 */
|
/** 返回材质、质量与预置共同确定的六个具体参数,调用方可以安全修改返回值。 */
|
||||||
export function getGlassOpticalPresetParameters(
|
export function getGlassOpticalPresetParameters(
|
||||||
appearance: GlassAppearance,
|
appearance: GlassAppearance,
|
||||||
quality: GlassOpticalCapability,
|
quality: GlassOpticalCapability,
|
||||||
@@ -277,18 +280,44 @@ export function getGlassOpticalReflectionStrengthScale(value: unknown) {
|
|||||||
return normalizedRatio ** Math.log2(GLASS_OPTICAL_REFLECTION_MAX_SCALE)
|
return normalizedRatio ** Math.log2(GLASS_OPTICAL_REFLECTION_MAX_SCALE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通透度独立控制真实背景与可读性遮罩的占比,高区间继续增强但逐步收敛。 */
|
/** 通透度独立控制真实背景与可读性遮罩的占比,高区间仍受 shader 可读性上限保护。 */
|
||||||
export function getGlassOpticalTransparency(value: unknown) {
|
export function getGlassOpticalTransparency(value: unknown) {
|
||||||
const normalized = normalizeGlassOpticalStrength(value)
|
const normalized = normalizeGlassOpticalStrength(value)
|
||||||
if (normalized <= 80) {
|
if (normalized <= GLASS_OPTICAL_REFERENCE_STRENGTH) {
|
||||||
const progress = normalized / 80
|
const progress = normalized / GLASS_OPTICAL_REFERENCE_STRENGTH
|
||||||
|
|
||||||
return 0.28 + 0.58 * progress ** 1.25
|
return 0.28 + 0.68 * progress ** 1.25
|
||||||
}
|
}
|
||||||
|
|
||||||
const highRangeProgress = (normalized - 80) / 20
|
const highRangeProgress =
|
||||||
|
(normalized - GLASS_OPTICAL_REFERENCE_STRENGTH) / (GLASS_OPTICAL_STRENGTH_MAX - GLASS_OPTICAL_REFERENCE_STRENGTH)
|
||||||
|
|
||||||
return 0.86 + 0.1 * highRangeProgress ** 1.5
|
return 0.96 + 0.14 * highRangeProgress ** 1.35
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标准 CSS 材质使用受控亮度曲线,并在高区间保留有限余量。 */
|
||||||
|
export function getGlassOpticalCssTransmissionBrightness(value: unknown) {
|
||||||
|
const transmission = getGlassOpticalTransmissionStrength(value)
|
||||||
|
if (transmission <= 1) {
|
||||||
|
return 0.82 + 0.46 * transmission ** 1.1
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = (transmission - 1) / 0.3
|
||||||
|
|
||||||
|
return 1.28 + 0.2 * progress ** 1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 实时 renderer 接收透射响应,并在 shader 中按材质和质量执行高亮保护。 */
|
||||||
|
export function getGlassOpticalTransmissionStrength(value: unknown) {
|
||||||
|
const normalized = normalizeGlassOpticalStrength(value)
|
||||||
|
if (normalized <= GLASS_OPTICAL_REFERENCE_STRENGTH) {
|
||||||
|
return normalized / GLASS_OPTICAL_REFERENCE_STRENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
const highRangeProgress =
|
||||||
|
(normalized - GLASS_OPTICAL_REFERENCE_STRENGTH) / (GLASS_OPTICAL_STRENGTH_MAX - GLASS_OPTICAL_REFERENCE_STRENGTH)
|
||||||
|
|
||||||
|
return 1 + 0.3 * highRangeProgress ** 1.2
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 质量决定合成缓冲与纹理上限;路由只切换纹理来源,不改变质量档位。 */
|
/** 质量决定合成缓冲与纹理上限;路由只切换纹理来源,不改变质量档位。 */
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import type { GlassAppearance, GlassOpticalPreset } from '@/utils/glassOptics'
|
||||||
|
|
||||||
|
export type LoginVisualProfile = 'classic' | 'glass' | 'transparent'
|
||||||
|
export type LoginWallpaperRequestMode = 'default' | 'same-origin'
|
||||||
|
|
||||||
|
export interface LoginGlassPreference {
|
||||||
|
/** 用户选择的玻璃材质。 */
|
||||||
|
appearance: GlassAppearance
|
||||||
|
/** 用户保存的局部非均匀形变强度。 */
|
||||||
|
deformationStrength: number
|
||||||
|
/** 用户保存的轨迹、尾波与惯性强度。 */
|
||||||
|
flowStrength: number
|
||||||
|
/** 用户选择的方案;登录页保持方案身份但只强制高质量能力。 */
|
||||||
|
preset: GlassOpticalPreset
|
||||||
|
/** 用户保存的方向反射亮度。 */
|
||||||
|
reflectionStrength: number
|
||||||
|
/** 用户保存的玻璃内部透射亮度。 */
|
||||||
|
transmissionStrength: number
|
||||||
|
/** 用户保存的统一采样平移强度。 */
|
||||||
|
translationStrength: number
|
||||||
|
/** 用户保存的壁纸可见度与材质遮罩强度。 */
|
||||||
|
transparencyStrength: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginBackgroundLayer {
|
||||||
|
/** 跨登录状态保持稳定的呈现槽位。 */
|
||||||
|
key: 'back' | 'front'
|
||||||
|
/** 壁纸在交叉淡化中的职责;standby 槽位可提前准备下一张。 */
|
||||||
|
role: 'active' | 'previous' | 'standby'
|
||||||
|
/** 当前槽位显示的壁纸地址。 */
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将实际主题解析为互斥的登录视觉 profile。 */
|
||||||
|
export function getLoginVisualProfile(themeName: string): LoginVisualProfile {
|
||||||
|
if (themeName === 'glass') return 'glass'
|
||||||
|
if (themeName === 'transparent') return 'transparent'
|
||||||
|
return 'classic'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 玻璃登录页只强制高质量能力,六个用户强度在登录前后保持一致。 */
|
||||||
|
export function getLoginGlassOpticalSettings(preference: LoginGlassPreference) {
|
||||||
|
return {
|
||||||
|
appearance: preference.appearance,
|
||||||
|
deformationStrength: preference.deformationStrength,
|
||||||
|
flowStrength: preference.flowStrength,
|
||||||
|
preset: preference.preset,
|
||||||
|
quality: 'high' as const,
|
||||||
|
reflectionStrength: preference.reflectionStrength,
|
||||||
|
transmissionStrength: preference.transmissionStrength,
|
||||||
|
translationStrength: preference.translationStrength,
|
||||||
|
transparencyStrength: preference.transparencyStrength,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 玻璃主题需要同源纹理,其余主题保留现有外链返回行为。 */
|
||||||
|
export function getLoginWallpaperRequestMode(profile: LoginVisualProfile): LoginWallpaperRequestMode {
|
||||||
|
return profile === 'glass' ? 'same-origin' : 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 建立两个始终存在的背景槽位,避免角色变化时复用错误的 DOM 合成层。 */
|
||||||
|
export function createLoginBackgroundLayers(activeUrl = ''): LoginBackgroundLayer[] {
|
||||||
|
return [
|
||||||
|
{ key: 'front', role: 'active', url: activeUrl },
|
||||||
|
{ key: 'back', role: 'standby', url: '' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把下一张壁纸放入隐藏槽位,不改变当前可见层。 */
|
||||||
|
export function prepareLoginBackgroundLayer(layers: LoginBackgroundLayer[], url: string): LoginBackgroundLayer[] {
|
||||||
|
return layers.map(layer => (layer.role === 'standby' ? { ...layer, url } : { ...layer }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在壁纸与纹理均就绪后原子交换两个槽位的职责。 */
|
||||||
|
export function activateLoginBackgroundLayer(layers: LoginBackgroundLayer[]): LoginBackgroundLayer[] {
|
||||||
|
if (!layers.some(layer => layer.role === 'standby' && layer.url)) return layers
|
||||||
|
|
||||||
|
return layers.map(layer => ({
|
||||||
|
...layer,
|
||||||
|
role: layer.role === 'active' ? 'previous' : layer.role === 'standby' ? 'active' : layer.role,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 交叉淡化完成后清空旧图,使该稳定槽位可准备下一次切换。 */
|
||||||
|
export function settleLoginBackgroundLayers(layers: LoginBackgroundLayer[]): LoginBackgroundLayer[] {
|
||||||
|
return layers.map(layer => (layer.role === 'previous' ? { ...layer, role: 'standby', url: '' } : { ...layer }))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user