feat(display): background photo groups with blurred, compressed and original versions; veil, sway and blur options in the appearance panel [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { APPEARANCE_DEFAULTS, APPEARANCE_RANGES, resetAppearance, setAppearance, useAppearance, type AppearanceSettings } from './appearance'
|
||||
import { PHOTO_GROUPS, PHOTOS } from './backgrounds'
|
||||
|
||||
type Tab = 'weather' | 'background'
|
||||
|
||||
@@ -8,6 +9,8 @@ type SliderName = keyof typeof APPEARANCE_RANGES
|
||||
interface Slider {
|
||||
name: SliderName
|
||||
label: string
|
||||
/** 悬停提示。 */
|
||||
hint?: string
|
||||
/** 数值怎么显示:默认是百分比。 */
|
||||
format?: (value: number) => string
|
||||
}
|
||||
@@ -29,17 +32,18 @@ const WEATHER_SWITCHES: { name: 'mouse' | 'bullet'; label: string; hint: string
|
||||
]
|
||||
|
||||
const BACKGROUND_SLIDERS: Slider[] = [
|
||||
{ name: 'bgBlur', label: '模糊', format: (value) => `${value}px` },
|
||||
{ name: 'bgBlur', label: '再模糊', format: (value) => `${value}px` },
|
||||
{ name: 'parallax', label: '晃动' },
|
||||
{ name: 'bgVeil', label: '遮罩', hint: '盖在背景图上的那层灰色(暗色)/ 白色(亮色),0% 就是不盖' },
|
||||
]
|
||||
|
||||
const WEATHER_KEYS: (keyof AppearanceSettings)[] = ['enabled', 'density', 'speed', 'wind', 'size', 'blur', 'opacity', 'bullet', 'mouse']
|
||||
const BACKGROUND_KEYS: (keyof AppearanceSettings)[] = ['bgBlur', 'parallax']
|
||||
const BACKGROUND_KEYS: (keyof AppearanceSettings)[] = ['photo', 'bgBlurred', 'bgBlur', 'bgOriginal', 'sway', 'parallax', 'bgVeil']
|
||||
|
||||
function SliderRow({ slider, settings, disabled }: { slider: Slider; settings: AppearanceSettings; disabled?: boolean }) {
|
||||
const { name, label, format = percent } = slider
|
||||
const { name, label, hint, format = percent } = slider
|
||||
return (
|
||||
<label className="appearance-row">
|
||||
<label className="appearance-row" title={hint}>
|
||||
<span>{label}</span>
|
||||
<input type="range" {...APPEARANCE_RANGES[name]} value={settings[name]} disabled={disabled} onChange={(event) => setAppearance({ [name]: Number(event.target.value) })} />
|
||||
<output>{format(settings[name])}</output>
|
||||
@@ -111,9 +115,31 @@ export function AppearancePanel() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="appearance-body" key="background">
|
||||
{BACKGROUND_SLIDERS.map((slider) => (
|
||||
<SliderRow key={slider.name} slider={slider} settings={settings} />
|
||||
{PHOTO_GROUPS.map((group) => (
|
||||
<div key={group} role="group" aria-label={group}>
|
||||
<div className="appearance-group-title">{group}</div>
|
||||
<div className="appearance-choices">
|
||||
{PHOTOS.filter((photo) => photo.group === group).map(({ id, label, light }) => (
|
||||
<button key={id} type="button" className="appearance-choice" aria-pressed={settings.photo === id} onClick={() => setAppearance({ photo: id })}>
|
||||
<img src={light.blur} alt="" loading="lazy" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<SwitchRow label="背景模糊" hint="关掉就换成不模糊的图" checked={settings.bgBlurred} onChange={(bgBlurred) => setAppearance({ bgBlurred })} />
|
||||
<SliderRow slider={BACKGROUND_SLIDERS[0]} settings={settings} disabled={!settings.bgBlurred} />
|
||||
<SwitchRow
|
||||
label="不压缩"
|
||||
hint="背景模糊关着时:开是加载没压缩的原图(每张约 2MB),关是压缩过的(不到 300KB)"
|
||||
checked={settings.bgOriginal}
|
||||
disabled={settings.bgBlurred}
|
||||
onChange={(bgOriginal) => setAppearance({ bgOriginal })}
|
||||
/>
|
||||
<SwitchRow label="背景晃动" hint="关掉就不跟着鼠标晃" checked={settings.sway} onChange={(sway) => setAppearance({ sway })} />
|
||||
<SliderRow slider={BACKGROUND_SLIDERS[1]} settings={settings} disabled={!settings.sway} />
|
||||
<SliderRow slider={BACKGROUND_SLIDERS[2]} settings={settings} />
|
||||
</div>
|
||||
)}
|
||||
<button type="button" className="appearance-reset" disabled={!changed} tabIndex={open ? 0 : -1} onClick={() => resetAppearance(keys)}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useSyncExternalStore } from 'react'
|
||||
import { backgroundUrl, DEFAULT_PHOTO, PHOTOS } from './backgrounds'
|
||||
|
||||
/**
|
||||
* 展示页右下角「外观」里能调的参数:都是相对默认值的倍数(1 = 原样),存在浏览器里。
|
||||
@@ -23,10 +24,20 @@ export interface AppearanceSettings {
|
||||
bullet: boolean
|
||||
/** 鼠标拨动雪。 */
|
||||
mouse: boolean
|
||||
/** 背景在预先模糊的基础上再模糊多少(像素,0 是不再模糊)。 */
|
||||
/** 用哪张照片(id,见 backgrounds.ts)。 */
|
||||
photo: string
|
||||
/** 背景模糊:开是随主题的两张预先模糊过的图,关是不模糊的图(两张,也是随主题)。 */
|
||||
bgBlurred: boolean
|
||||
/** 背景模糊关着的时候:开是加载没压缩的原图(PNG,近 2MB 一张),关是压缩过的(JPG,不到 300KB)。 */
|
||||
bgOriginal: boolean
|
||||
/** 背景模糊开着的时候,在预先模糊的基础上再模糊多少(像素,0 是不再模糊)。 */
|
||||
bgBlur: number
|
||||
/** 背景跟着鼠标晃动的幅度(0 是不动,1 是默认)。 */
|
||||
/** 背景跟着鼠标晃动:总开关,关了背景就完全不动。 */
|
||||
sway: boolean
|
||||
/** 背景跟着鼠标晃动的幅度(1 是默认)。 */
|
||||
parallax: number
|
||||
/** 盖在背景图上的那层遮罩(亮色是白、暗色是灰)的浓度:0 是不盖,1 是默认(亮色 50%、暗色 62%)。 */
|
||||
bgVeil: number
|
||||
}
|
||||
|
||||
export const APPEARANCE_DEFAULTS: AppearanceSettings = {
|
||||
@@ -39,8 +50,13 @@ export const APPEARANCE_DEFAULTS: AppearanceSettings = {
|
||||
opacity: 1,
|
||||
bullet: true,
|
||||
mouse: true,
|
||||
photo: DEFAULT_PHOTO,
|
||||
bgBlurred: true,
|
||||
bgOriginal: false,
|
||||
bgBlur: 0,
|
||||
sway: true,
|
||||
parallax: 1,
|
||||
bgVeil: 1,
|
||||
}
|
||||
|
||||
/** 每个滑块的范围(和 AppearanceSettings 里同名的数值项对应)。 */
|
||||
@@ -53,6 +69,7 @@ export const APPEARANCE_RANGES = {
|
||||
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 },
|
||||
bgVeil: { min: 0, max: 1.6, step: 0.05 },
|
||||
} as const
|
||||
|
||||
const KEY = 'moshiro.appearance'
|
||||
@@ -64,7 +81,10 @@ function load(): 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
|
||||
if (typeof value !== typeof APPEARANCE_DEFAULTS[name]) continue
|
||||
// 照片只认列表里有的,图删了、改名了就回到默认
|
||||
if (name === 'photo' && !PHOTOS.some((photo) => photo.id === value)) continue
|
||||
merged[name] = value
|
||||
}
|
||||
return merged as unknown as AppearanceSettings
|
||||
} catch {
|
||||
@@ -96,14 +116,28 @@ export function subscribeAppearance(listener: () => void): () => void {
|
||||
|
||||
export const useAppearance = () => useSyncExternalStore(subscribeAppearance, getAppearance)
|
||||
|
||||
/** 把背景的额外模糊写进 <html> 的 --bg-filter(display.css 里 .bg-layer 用它;不模糊的时候是 none,不给浏览器多一层滤镜)。 */
|
||||
/**
|
||||
* 把当前该用的背景图写进 <html> 的 --bg-image(照片、主题、背景模糊、不压缩一起决定用哪个版本;display.css 里 .bg-layer 用它),
|
||||
* 把遮罩的浓度写进 --bg-veil-scale,把背景的额外模糊写进 --bg-filter(不模糊的时候是 none,不给浏览器多一层滤镜),
|
||||
* 把晃动的开关写进 data-sway(关了就按窗口的原始比例铺满,display.css 里 data-sway='off')。切换主题时也重新算。
|
||||
*/
|
||||
export function useAppearanceVars() {
|
||||
useEffect(() => {
|
||||
const root = document.documentElement
|
||||
const apply = () => {
|
||||
const blur = getAppearance().bgBlur
|
||||
document.documentElement.style.setProperty('--bg-filter', blur > 0 ? `blur(${blur}px)` : 'none')
|
||||
const { bgBlur, bgBlurred, bgOriginal, photo, bgVeil, sway } = getAppearance()
|
||||
root.style.setProperty('--bg-veil-scale', String(bgVeil))
|
||||
root.style.setProperty('--bg-filter', bgBlurred && bgBlur > 0 ? `blur(${bgBlur}px)` : 'none')
|
||||
root.style.setProperty('--bg-image', `url("${backgroundUrl(photo, root.dataset.theme === 'dark', bgBlurred, bgOriginal)}")`)
|
||||
root.dataset.sway = sway ? 'on' : 'off'
|
||||
}
|
||||
apply()
|
||||
return subscribeAppearance(apply)
|
||||
const observer = new MutationObserver(apply)
|
||||
observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] })
|
||||
const unsubscribe = subscribeAppearance(apply)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 483 KiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 348 KiB |
|
After Width: | Height: | Size: 314 KiB |
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
import bgLight from './assets/bg.jpg'
|
||||
import bgDark from './assets/bg-dark.jpg'
|
||||
import { getAppearance, subscribeAppearance } from './appearance'
|
||||
import { backgroundUrl } from './backgrounds'
|
||||
|
||||
/** 取样的缩略图大小:背景本来就是预先模糊过的,缩得很小就够算平均色。 */
|
||||
const SAMPLE_WIDTH = 32
|
||||
@@ -46,8 +46,8 @@ function toHsl(r: number, g: number, b: number): [number, number, number] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 暗色下玻璃的颜色,和盖在背景图上的底色,都从当前背景图自己的平均色算出来:
|
||||
* 色相照搬图里的,饱和度打个折(免得发艳、发蓝),亮度压到很暗。写进 <html> 的 --bg-tint(玻璃)和 --bg-veil-tint(底色),
|
||||
* 暗色下玻璃的颜色,和盖在背景图上的底色,都从当前背景图(选了哪组照片、哪个主题)自己的平均色算出来:
|
||||
* 色相照搬图里的,饱和度保留大半,亮度跟着图本身的亮度走但压得很暗。写进 <html> 的 --bg-tint(玻璃)和 --bg-veil-tint(底色),
|
||||
* tokens.css 里暗色的 --glass-tint / --glass-veil 用它们;切换主题时重新取。亮色不用这个。
|
||||
*/
|
||||
export function useBackgroundTint() {
|
||||
@@ -58,12 +58,18 @@ export function useBackgroundTint() {
|
||||
const image = images.get(url)
|
||||
const average = image && averageColor(image)
|
||||
if (!average) return
|
||||
const [h, s] = toHsl(...average)
|
||||
root.style.setProperty('--bg-tint', `hsl(${h.toFixed(0)} ${(s * 55).toFixed(0)}% 13%)`)
|
||||
root.style.setProperty('--bg-veil-tint', `hsl(${h.toFixed(0)} ${(s * 45).toFixed(0)}% 8%)`)
|
||||
const [h, s, l] = toHsl(...average)
|
||||
// 玻璃:色相照搬,饱和度保留大半(上限 45%),亮度跟着图本身的亮度走(图越亮,玻璃越亮,10%–22%);
|
||||
// 底色(盖在背景图上的遮罩)同理,但更暗(6%–14%)。所以换一张照片,暗色玻璃的颜色和深浅都会跟着变
|
||||
const clamp = (value: number, low: number, high: number) => Math.max(low, Math.min(high, value))
|
||||
const glass = clamp(9 + l * 26, 10, 22)
|
||||
const veil = clamp(5 + l * 14, 6, 14)
|
||||
root.style.setProperty('--bg-tint', `hsl(${h.toFixed(0)} ${clamp(s * 90, 0, 45).toFixed(0)}% ${glass.toFixed(1)}%)`)
|
||||
root.style.setProperty('--bg-veil-tint', `hsl(${h.toFixed(0)} ${clamp(s * 75, 0, 38).toFixed(0)}% ${veil.toFixed(1)}%)`)
|
||||
}
|
||||
const update = () => {
|
||||
const url = root.dataset.theme === 'dark' ? bgDark : bgLight
|
||||
// 取样用预先模糊的那个版本:和不模糊的原图平均色一样,图小、取得快
|
||||
const url = backgroundUrl(getAppearance().photo, root.dataset.theme === 'dark', true, false)
|
||||
if (images.has(url)) {
|
||||
apply(url)
|
||||
return
|
||||
@@ -76,6 +82,10 @@ export function useBackgroundTint() {
|
||||
update()
|
||||
const observer = new MutationObserver(update)
|
||||
observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] })
|
||||
return () => observer.disconnect()
|
||||
const unsubscribe = subscribeAppearance(update)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import bgBlurLight from './assets/bg.jpg'
|
||||
import bgBlurDark from './assets/bg-dark.jpg'
|
||||
import bgSharpLight from './assets/bg-sharp.jpg'
|
||||
import bgSharpDark from './assets/bg-sharp-dark.jpg'
|
||||
import bgOriginalLight from './assets/bg-sharp.png'
|
||||
import bgOriginalDark from './assets/bg-sharp-dark.png'
|
||||
import banaBlur from './assets/bg-bana.jpg'
|
||||
import banaSharp from './assets/bg-bana-sharp.jpg'
|
||||
import banaOriginal from './assets/bg-bana-sharp.png'
|
||||
import bana2Blur from './assets/bg-bana2.jpg'
|
||||
import bana2Sharp from './assets/bg-bana2-sharp.jpg'
|
||||
import bana2Original from './assets/bg-bana2-sharp.png'
|
||||
import photo86_3Blur from './assets/photo86-3-blur.jpg'
|
||||
import photo86_3 from './assets/photo86-3.jpg'
|
||||
import photo86_3Original from './assets/photo86-3-original.png'
|
||||
import photo86_7Blur from './assets/photo86-7-blur.jpg'
|
||||
import photo86_7 from './assets/photo86-7.jpg'
|
||||
import photo86_7Original from './assets/photo86-7-original.jpg'
|
||||
import photo86_2Blur from './assets/photo86-2-blur.jpg'
|
||||
import photo86_2 from './assets/photo86-2.jpg'
|
||||
import photo86_2Original from './assets/photo86-2-original.jpg'
|
||||
import photo86_8Blur from './assets/photo86-8-blur.jpg'
|
||||
import photo86_8 from './assets/photo86-8.jpg'
|
||||
import photo86_8Original from './assets/photo86-8-original.jpg'
|
||||
import photo86_5Blur from './assets/photo86-5-blur.jpg'
|
||||
import photo86_5 from './assets/photo86-5.jpg'
|
||||
import photo86_5Original from './assets/photo86-5-original.jpg'
|
||||
import photo86_6Blur from './assets/photo86-6-blur.jpg'
|
||||
import photo86_6 from './assets/photo86-6.jpg'
|
||||
import photo86_6Original from './assets/photo86-6-original.jpg'
|
||||
|
||||
/** 一张照片的三个版本:blur 是预先模糊过的,sharp 是不模糊但压缩过的,original 是没压缩的原图。 */
|
||||
export interface Variants {
|
||||
blur: string
|
||||
sharp: string
|
||||
original: string
|
||||
}
|
||||
|
||||
/** 可选的背景照片:group 是分组(面板里一组一行),亮色和暗色可以是不同的两张,也可以是同一张。 */
|
||||
export interface Photo {
|
||||
id: string
|
||||
group: string
|
||||
label: string
|
||||
light: Variants
|
||||
dark: Variants
|
||||
}
|
||||
|
||||
const same = (variants: Variants) => ({ light: variants, dark: variants })
|
||||
|
||||
/** 加一张照片:把它的三个版本放进 assets/,在这里加一项。第一项是默认的。 */
|
||||
export const PHOTOS: Photo[] = [
|
||||
{
|
||||
id: 'sleep',
|
||||
group: 'shirobana',
|
||||
label: '睡颜',
|
||||
// 亮色睁眼、暗色闭眼
|
||||
light: { blur: bgBlurLight, sharp: bgSharpLight, original: bgOriginalLight },
|
||||
dark: { blur: bgBlurDark, sharp: bgSharpDark, original: bgOriginalDark },
|
||||
},
|
||||
{ id: 'bana', group: 'shirobana', label: '伸手', ...same({ blur: banaBlur, sharp: banaSharp, original: banaOriginal }) },
|
||||
{ id: 'bana2', group: 'shirobana', label: '海边', ...same({ blur: bana2Blur, sharp: bana2Sharp, original: bana2Original }) },
|
||||
{ id: '86-3', group: '86', label: '小憩', ...same({ blur: photo86_3Blur, sharp: photo86_3, original: photo86_3Original }) },
|
||||
{ id: '86-7', group: '86', label: '窗边', ...same({ blur: photo86_7Blur, sharp: photo86_7, original: photo86_7Original }) },
|
||||
{ id: '86-2', group: '86', label: '伞下', ...same({ blur: photo86_2Blur, sharp: photo86_2, original: photo86_2Original }) },
|
||||
{ id: '86-8', group: '86', label: '驾驶', ...same({ blur: photo86_8Blur, sharp: photo86_8, original: photo86_8Original }) },
|
||||
{ id: '86-5', group: '86', label: '怀中', ...same({ blur: photo86_5Blur, sharp: photo86_5, original: photo86_5Original }) },
|
||||
{ id: '86-6', group: '86', label: '倚靠', ...same({ blur: photo86_6Blur, sharp: photo86_6, original: photo86_6Original }) },
|
||||
]
|
||||
|
||||
export const DEFAULT_PHOTO = PHOTOS[0].id
|
||||
|
||||
/** 照片所在的分组,按出现的顺序。 */
|
||||
export const PHOTO_GROUPS = [...new Set(PHOTOS.map((photo) => photo.group))]
|
||||
|
||||
export const findPhoto = (id: string): Photo => PHOTOS.find((photo) => photo.id === id) ?? PHOTOS[0]
|
||||
|
||||
/** 当前该用哪张图:哪张照片、哪个主题,再按「背景模糊」「不压缩」选版本。 */
|
||||
export function backgroundUrl(id: string, dark: boolean, blurred: boolean, original: boolean): string {
|
||||
const variants = dark ? findPhoto(id).dark : findPhoto(id).light
|
||||
return blurred ? variants.blur : original ? variants.original : variants.sharp
|
||||
}
|
||||
@@ -1008,7 +1008,7 @@
|
||||
z-index: -1;
|
||||
background-image:
|
||||
linear-gradient(color-mix(in srgb, var(--glass-veil) var(--bg-veil), transparent), color-mix(in srgb, var(--glass-veil) var(--bg-veil), transparent)),
|
||||
url('./assets/bg.jpg');
|
||||
var(--bg-image, url('./assets/bg.jpg'));
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
pointer-events: none;
|
||||
@@ -1017,18 +1017,24 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-veil: 50%;
|
||||
/* 盖在背景图上的遮罩的浓度:默认亮色 50%、暗色 62%;右下角「外观」里按倍数调(--bg-veil-scale,0 是不盖) */
|
||||
--bg-veil: min(100%, calc(50% * var(--bg-veil-scale, 1)));
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--bg-veil: 62%;
|
||||
--bg-veil: min(100%, calc(62% * var(--bg-veil-scale, 1)));
|
||||
}
|
||||
|
||||
/* 暗色模式换一张背景图 */
|
||||
/* 用哪张图由「外观」里的选择决定(backgrounds.ts,JS 把它写进 --bg-image);这里的回退只管 JS 还没跑的第一帧:暗色先用暗色那张 */
|
||||
:root[data-theme="dark"] .bg-layer {
|
||||
background-image:
|
||||
linear-gradient(color-mix(in srgb, var(--glass-veil) var(--bg-veil), transparent), color-mix(in srgb, var(--glass-veil) var(--bg-veil), transparent)),
|
||||
url('./assets/bg-dark.jpg');
|
||||
var(--bg-image, url('./assets/bg-dark.jpg'));
|
||||
}
|
||||
|
||||
/* 关了背景晃动:背景不会再移动,就不用向外多留一圈边距了,按窗口的原始比例铺满(不会被放大裁掉四周) */
|
||||
:root[data-sway='off'] .bg-layer {
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* ── 亚克力(毛玻璃)规范:所有玻璃面(卡片、线路面板、顶部栏、时间轴)只用下面这组变量;颜色来自 tokens.css 的 --glass-*,换风格改那里 ──
|
||||
@@ -1313,8 +1319,8 @@ input {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 悬浮提示弹在卡片右边,会盖到下一张卡片上:每张卡片都有自己的层叠上下文(毛玻璃),所以悬停(提示打开)的那张要整张抬到兄弟卡片之上 */
|
||||
.agent-card:hover,
|
||||
/* 悬浮提示弹在卡片右边,会盖到下一张卡片上:每张卡片都有自己的层叠上下文(毛玻璃),所以提示打开的那张要整张抬到兄弟卡片之上。
|
||||
只在提示真的打开时才抬,不要鼠标一碰到卡片就抬:z-index 一变,带 backdrop-filter 的整张卡片要重新分层,那一帧底部会闪黑 */
|
||||
.agent-card:has(.pop.open) {
|
||||
z-index: 30;
|
||||
}
|
||||
@@ -1393,8 +1399,7 @@ input {
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
/* 滤色:粒子只会把背后的图提亮,边缘羽化的地方自然融进图里 */
|
||||
mix-blend-mode: screen;
|
||||
/* 不要用 mix-blend-mode(滤色等):画布在带 backdrop-filter 的卡片后面每帧都在变,再叠一层混合,Chrome 给卡片取背景的时候会整块重新分层,卡片底部就闪黑;边缘的羽化用颜色和透明度做,已经是浅色半透明的,效果和滤色差不多 */
|
||||
}
|
||||
|
||||
@media (max-width: 640px), ((pointer: coarse) and (max-width: 900px)) {
|
||||
@@ -1657,3 +1662,55 @@ input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 照片的选择:按组分行,每组一个小标题,下面是缩略图,选中的描一圈 */
|
||||
.appearance-group-title {
|
||||
margin-bottom: 4px;
|
||||
color: var(--app-ink-faint);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.appearance-choices {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.appearance-choice {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: var(--app-ink-muted);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.appearance-choice img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
border-radius: var(--r-control);
|
||||
box-shadow: inset 0 0 0 1px var(--glass-ring);
|
||||
transition:
|
||||
box-shadow 0.2s,
|
||||
transform 0.2s;
|
||||
}
|
||||
|
||||
.appearance-choice:hover img {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.appearance-choice[aria-pressed='true'] {
|
||||
color: var(--app-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.appearance-choice[aria-pressed='true'] img {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--app-primary),
|
||||
0 0 0 4px color-mix(in srgb, var(--app-primary) 25%, transparent);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, type RefObject } from 'react'
|
||||
import { getAppearance } from './appearance'
|
||||
import { getAppearance, subscribeAppearance } from './appearance'
|
||||
|
||||
/** 指针在窗口里的位置带来的偏移上限(像素):鼠标停在窗口边缘时背景偏这么多。 */
|
||||
const POSITION_SHIFT = 30
|
||||
@@ -53,7 +53,7 @@ export function useBackgroundParallax(layer: RefObject<HTMLElement | null>) {
|
||||
const clamp = (value: number) => Math.max(-SWAY_MAX, Math.min(SWAY_MAX, value))
|
||||
const move = (event: PointerEvent) => {
|
||||
// 位置:指针在窗口里的位置换成 -1 到 1(背景朝同一个方向偏移,想要反方向就把符号取反)
|
||||
const amount = getAppearance().parallax
|
||||
const amount = swayAmount()
|
||||
baseX = ((event.clientX / window.innerWidth) * 2 - 1) * POSITION_SHIFT * amount
|
||||
baseY = ((event.clientY / window.innerHeight) * 2 - 1) * POSITION_SHIFT * amount
|
||||
// 晃动:这一次移动了多远,就再推背景一把
|
||||
@@ -65,6 +65,11 @@ export function useBackgroundParallax(layer: RefObject<HTMLElement | null>) {
|
||||
lastY = event.clientY
|
||||
schedule()
|
||||
}
|
||||
// 晃动的幅度:总开关关了就是 0
|
||||
const swayAmount = () => {
|
||||
const { sway, parallax } = getAppearance()
|
||||
return sway ? parallax : 0
|
||||
}
|
||||
const leave = () => {
|
||||
baseX = 0
|
||||
baseY = 0
|
||||
@@ -75,7 +80,16 @@ export function useBackgroundParallax(layer: RefObject<HTMLElement | null>) {
|
||||
|
||||
window.addEventListener('pointermove', move, { passive: true })
|
||||
document.documentElement.addEventListener('mouseleave', leave)
|
||||
// 设置里把晃动关了:背景慢慢回到中间,不用等下一次鼠标移动
|
||||
const unsubscribe = subscribeAppearance(() => {
|
||||
if (swayAmount() === 0) {
|
||||
swayX = 0
|
||||
swayY = 0
|
||||
leave()
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
unsubscribe()
|
||||
window.removeEventListener('pointermove', move)
|
||||
document.documentElement.removeEventListener('mouseleave', leave)
|
||||
cancelAnimationFrame(frame)
|
||||
|
||||