feat(display): falling snow in three depth layers with wind, mouse and bullet time; appearance panel; dark glass tinted from the background [CI SKIP]
This commit is contained in:
1 parent
f8e8bb8acb
commit
e49990776b
10 files changed
+1003
-7
No files matched your search
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useSyncExternalStore } from 'react'
|
||||
|
||||
/**
|
||||
* 展示页右下角「外观」里能调的参数:都是相对默认值的倍数(1 = 原样),存在浏览器里。
|
||||
* 分两类:天气(雪,见 weather/)和背景(模糊、跟着鼠标晃动)。
|
||||
*/
|
||||
export interface AppearanceSettings {
|
||||
/** 下雪的总开关。 */
|
||||
enabled: boolean
|
||||
/** 数量。 */
|
||||
density: number
|
||||
/** 下落速度。 */
|
||||
speed: number
|
||||
/** 风力(风的大小和鼠标带起的风)。 */
|
||||
wind: number
|
||||
/** 粒子大小。 */
|
||||
size: number
|
||||
/** 虚化程度(1 是预设原样,小于 1 更清晰,大于 1 更糊)。 */
|
||||
blur: number
|
||||
/** 透明度。 */
|
||||
opacity: number
|
||||
/** 子弹时间(鼠标停住时周围变慢)。 */
|
||||
bullet: boolean
|
||||
/** 鼠标拨动雪。 */
|
||||
mouse: boolean
|
||||
/** 背景在预先模糊的基础上再模糊多少(像素,0 是不再模糊)。 */
|
||||
bgBlur: number
|
||||
/** 背景跟着鼠标晃动的幅度(0 是不动,1 是默认)。 */
|
||||
parallax: number
|
||||
}
|
||||
|
||||
export const APPEARANCE_DEFAULTS: AppearanceSettings = {
|
||||
enabled: true,
|
||||
density: 1,
|
||||
speed: 1,
|
||||
wind: 1,
|
||||
size: 1,
|
||||
blur: 1,
|
||||
opacity: 1,
|
||||
bullet: true,
|
||||
mouse: true,
|
||||
bgBlur: 0,
|
||||
parallax: 1,
|
||||
}
|
||||
|
||||
/** 每个滑块的范围(和 AppearanceSettings 里同名的数值项对应)。 */
|
||||
export const APPEARANCE_RANGES = {
|
||||
density: { min: 0.2, max: 3, step: 0.05 },
|
||||
speed: { min: 0.3, max: 3, step: 0.05 },
|
||||
wind: { min: 0, max: 3, step: 0.05 },
|
||||
size: { min: 0.5, max: 2.5, step: 0.05 },
|
||||
blur: { min: 0, max: 2, step: 0.05 },
|
||||
opacity: { min: 0.2, max: 2.5, step: 0.05 },
|
||||
bgBlur: { min: 0, max: 30, step: 1 },
|
||||
parallax: { min: 0, max: 1.5, step: 0.05 },
|
||||
} as const
|
||||
|
||||
const KEY = 'moshiro.appearance'
|
||||
const listeners = new Set<() => void>()
|
||||
|
||||
function load(): AppearanceSettings {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(KEY) ?? '{}') as Partial<AppearanceSettings>
|
||||
const merged: Record<string, unknown> = { ...APPEARANCE_DEFAULTS }
|
||||
for (const name of Object.keys(APPEARANCE_DEFAULTS) as (keyof AppearanceSettings)[]) {
|
||||
const value = saved[name]
|
||||
if (typeof value === typeof APPEARANCE_DEFAULTS[name]) merged[name] = value
|
||||
}
|
||||
return merged as unknown as AppearanceSettings
|
||||
} catch {
|
||||
return { ...APPEARANCE_DEFAULTS }
|
||||
}
|
||||
}
|
||||
|
||||
let current = load()
|
||||
|
||||
export const getAppearance = () => current
|
||||
|
||||
export function setAppearance(patch: Partial<AppearanceSettings>) {
|
||||
current = { ...current, ...patch }
|
||||
try {
|
||||
localStorage.setItem(KEY, JSON.stringify(current))
|
||||
} catch {
|
||||
// 存不了(隐私模式等)就只在这次打开期间有效
|
||||
}
|
||||
listeners.forEach((listener) => listener())
|
||||
}
|
||||
|
||||
export const resetAppearance = (keys: (keyof AppearanceSettings)[]) =>
|
||||
setAppearance(Object.fromEntries(keys.map((name) => [name, APPEARANCE_DEFAULTS[name]])) as Partial<AppearanceSettings>)
|
||||
|
||||
export function subscribeAppearance(listener: () => void): () => void {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
}
|
||||
|
||||
export const useAppearance = () => useSyncExternalStore(subscribeAppearance, getAppearance)
|
||||
|
||||
/** 把背景的额外模糊写进 <html> 的 --bg-filter(display.css 里 .bg-layer 用它;不模糊的时候是 none,不给浏览器多一层滤镜)。 */
|
||||
export function useAppearanceVars() {
|
||||
useEffect(() => {
|
||||
const apply = () => {
|
||||
const blur = getAppearance().bgBlur
|
||||
document.documentElement.style.setProperty('--bg-filter', blur > 0 ? `blur(${blur}px)` : 'none')
|
||||
}
|
||||
apply()
|
||||
return subscribeAppearance(apply)
|
||||
}, [])
|
||||
}
|
||||
Reference in new issue
Block a user