diff --git a/apps/overlay/src/api.ts b/apps/overlay/src/api.ts index 2b5f0ee..80f8130 100644 --- a/apps/overlay/src/api.ts +++ b/apps/overlay/src/api.ts @@ -24,6 +24,7 @@ import type { TotpEnrollment, } from './types' import { normalizeThemeId } from './themes' +import { normalizeFontFamilyId } from './typography' import { normalizeGiftEffectThemeId } from './giftThemes' import { normalizeGiftMenuThemeId } from './giftMenuThemes' import { @@ -212,6 +213,7 @@ export function normalizeSettings(value: unknown): OverlaySettings { ...defaultOverlaySettings, ...(settings as Partial), themeId: normalizeThemeId(settings.themeId), + fontFamily: normalizeFontFamilyId(settings.fontFamily), } } @@ -222,6 +224,7 @@ export function normalizeSongRequestSettings(value: unknown): SongRequestSetting ...defaultSongRequestSettings, ...(settings as Partial), themeId: normalizeThemeId(settings.themeId), + fontFamily: normalizeFontFamilyId(settings.fontFamily), } } @@ -235,6 +238,7 @@ export function normalizeGiftEffectSettings(value: unknown): GiftEffectSettings ...defaultGiftEffectSettings, ...(settings as Partial), themeId: normalizeGiftEffectThemeId(settings.themeId), + fontFamily: normalizeFontFamilyId(settings.fontFamily), normal: { ...defaultGiftEffectSettings.normal, ...normal }, high: { ...defaultGiftEffectSettings.high, ...high }, featured: { ...defaultGiftEffectSettings.featured, ...featured }, @@ -295,6 +299,7 @@ export function normalizeGiftMenuSettings(value: unknown): GiftMenuSettings { ...defaultGiftMenuSettings, ...(settings as Partial), themeId: normalizeGiftMenuThemeId(settings.themeId), + fontFamily: normalizeFontFamilyId(settings.fontFamily), items, } } diff --git a/apps/overlay/src/control.css b/apps/overlay/src/control.css index 69262d8..858a26b 100644 --- a/apps/overlay/src/control.css +++ b/apps/overlay/src/control.css @@ -1,6 +1,7 @@ .overlay { align-items: flex-start; background: transparent; + font-family: var(--component-font-family, 'Noto Serif SC', 'Songti SC', 'STSong', serif); } .theme-jade-scroll { @@ -366,8 +367,7 @@ html, body, -#root, -.copy { +#root { font-family: 'Noto Serif SC', 'Microsoft YaHei', 'Noto Color Emoji', 'Segoe UI Emoji', 'Apple Color Emoji', serif; @@ -555,12 +555,24 @@ body, } .card > .gift-art, +.card > .viewer-avatar, .card > .copy, .card > .particles { position: relative; z-index: 3; } +.viewer-avatar { + display: none; +} + +.viewer-avatar img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + .card-particle-layer { position: absolute; inset: 0; @@ -720,7 +732,9 @@ body, } .copy { + font-family: var(--component-font-family, 'Noto Serif SC', 'Songti SC', 'STSong', serif); font-size: var(--font-body, 25px); + filter: brightness(var(--component-font-brightness, 1.3)); } .copy span { @@ -960,6 +974,267 @@ body, box-shadow: 0 0 0 2px rgba(137, 255, 226, 0.28); } +/* -------------------------------------------------------------------------- */ +/* Moonlit Water: text first, alpha always preserved */ +/* -------------------------------------------------------------------------- */ + +.overlay.theme-moonlit-water { + align-items: flex-start; + justify-content: flex-start; + padding: clamp(14px, 2.3vw, 34px); + background: transparent; + font-family: var(--component-font-family, FangSong, STFangsong, 'Noto Serif SC', serif); +} + +.theme-moonlit-water .wall { + width: min(100%, 620px); + gap: 0; + filter: none; +} + +.theme-moonlit-water .cards { + gap: clamp(24px, 3.8vh, 46px); +} + +.theme-moonlit-water .card { + min-height: 0; + gap: clamp(8px, 1.1vw, 14px); + padding: clamp(8px, 1.1vw, 15px) clamp(4px, 0.7vw, 10px); + border: 0; + border-bottom: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; +} + +.theme-moonlit-water .card::before { + display: none; +} + +.theme-moonlit-water .card-decor::before, +.theme-moonlit-water .card-decor::after, +.theme-moonlit-water .card-decor-surface { + display: none; +} + +.theme-moonlit-water .card-particle-layer { + opacity: 0.34; +} + +.theme-moonlit-water .card-particle.star { + background: #e7c982; + filter: drop-shadow(0 0 3px rgba(231, 201, 130, 0.58)); +} + +.theme-moonlit-water .card-particle.floret { + background: + radial-gradient(circle at 50% 17%, #d9e0c1 0 19%, transparent 22%), + radial-gradient(circle at 82% 50%, #d9e0c1 0 19%, transparent 22%), + radial-gradient(circle at 50% 83%, #d9e0c1 0 19%, transparent 22%), + radial-gradient(circle at 18% 50%, #d9e0c1 0 19%, transparent 22%), + radial-gradient(circle at 50% 50%, #e7c982 0 18%, transparent 21%); + filter: none; +} + +.theme-moonlit-water .copy { + color: #c6ae7a; + font-family: var(--component-font-family, FangSong, STFangsong, 'Noto Serif SC', serif); + letter-spacing: 0.075em; + line-height: 1.55; + text-shadow: 0 1px 6px rgba(10, 34, 38, 0.82); +} + +.theme-moonlit-water .copy b { + color: #b7ad83; + font-weight: 600; + letter-spacing: 0.14em; +} + +.theme-moonlit-water .copy span { + color: #c6ae7a; + letter-spacing: 0.075em; + line-height: 1.55; +} + +.theme-moonlit-water .card.danmaku .copy { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + grid-template-rows: auto auto; + align-items: center; + gap: 0.16em 0.56em; + width: 100%; +} + +.theme-moonlit-water .card.danmaku .copy > b { + grid-column: 2; + grid-row: 1; + line-height: 1.25; +} + +.theme-moonlit-water .card.danmaku .copy > .danmaku-content { + grid-column: 1 / -1; + grid-row: 2; + line-height: 1.35; +} + +.theme-moonlit-water .card.danmaku .viewer-avatar { + display: grid; + grid-column: 1; + grid-row: 1; + flex: 0 0 auto; + align-self: center; + width: clamp(38px, 2.3em, 56px); + height: clamp(38px, 2.3em, 56px); + overflow: hidden; + place-items: center; + border: 1px solid rgba(231, 201, 130, 0.58); + border-radius: 50%; + background: radial-gradient(circle at 36% 28%, rgba(224, 233, 205, 0.32), rgba(22, 72, 76, 0.5)); + color: #d2b976; + font-size: 0.86em; + font-weight: 600; + line-height: 1; + box-shadow: + 0 0 0 2px rgba(22, 72, 76, 0.28), + 0 0 12px rgba(231, 201, 130, 0.18); + transition: + width 0.28s ease, + height 0.28s ease; +} + +.theme-moonlit-water .card.danmaku .viewer-avatar img { + filter: saturate(0.84) brightness(1.06); +} + +.theme-moonlit-water .copy em, +.theme-moonlit-water .particles { + color: #d2ae5e; +} + +.theme-moonlit-water .gift-art { + width: clamp(38px, 4.2vw, 58px); + height: clamp(38px, 4.2vw, 58px); + border-radius: 0; + background: transparent; +} + +.theme-moonlit-water .gift-art img { + filter: saturate(0.76) sepia(0.1) brightness(1.08) drop-shadow(0 0 6px rgba(231, 201, 130, 0.44)); +} + +.theme-moonlit-water .gift.high, +.theme-moonlit-water .gift.featured { + min-height: 0; + border-color: rgba(231, 201, 130, 0.47); + background: transparent; + animation: moonlit-gift-breathe 3.4s ease-in-out infinite; +} + +.theme-moonlit-water .gift.featured .gift-art { + width: clamp(46px, 5vw, 68px); + height: clamp(46px, 5vw, 68px); +} + +.theme-moonlit-water .card.danmaku { + transition: padding 0.28s ease; +} + +.theme-moonlit-water .card.danmaku.expanded, +.theme-moonlit-water .card.danmaku.compact { + min-height: 0; + padding: clamp(10px, 1.25vw, 18px) clamp(4px, 0.7vw, 10px); + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.theme-moonlit-water .card.danmaku.expanded { + animation: moonlit-reveal var(--unfold-duration, 1000ms) cubic-bezier(0.2, 0.75, 0.25, 1) both; +} + +.theme-moonlit-water .card.danmaku.expanded::after { + display: none; +} + +.theme-moonlit-water .card.danmaku.expanded .copy { + gap: 0.16em 0.56em; + padding: 0; + font-size: var(--font-expanded, 36px); +} + +.theme-moonlit-water .card.danmaku.compact .copy { + gap: 0.14em 0.52em; + font-size: var(--font-compact, 21px); +} + +.theme-moonlit-water .card.danmaku.compact .copy > .danmaku-content { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; +} + +.theme-moonlit-water .card.danmaku.compact .viewer-avatar { + width: clamp(32px, 1.9em, 44px); + height: clamp(32px, 1.9em, 44px); +} + +.theme-moonlit-water .danmaku-emoticon { + filter: drop-shadow(0 1px 3px rgba(10, 34, 38, 0.56)); +} + +.theme-moonlit-water .particles { + opacity: 0.76; +} + +.theme-moonlit-water .card.expanded .card-particle-layer { + opacity: 0.64; +} + +@keyframes moonlit-reveal { + from { + clip-path: inset(0 100% 0 0); + } + to { + clip-path: inset(0 0 0 0); + } +} + +@keyframes moonlit-rule-reveal { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} + +@keyframes moonlit-gift-breathe { + 50% { + border-color: rgba(231, 201, 130, 0.8); + filter: brightness(1.14); + } +} + +@keyframes moonlit-drift { + 50% { + opacity: 0.46; + transform: translateY(-4px); + } +} + +@keyframes moonlit-sparkle { + 0%, + 100% { + opacity: 0.03; + transform: translate3d(0, 2px, 0) rotate(0) scale(0.4); + } + 44% { + opacity: 0.76; + transform: translate3d(1px, -2px, 0) rotate(32deg) scale(0.95); + } +} + .control .obs-address { margin-top: 16px; } @@ -1749,6 +2024,28 @@ select { margin-bottom: 24px; } +.typography-settings { + max-width: 780px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-bottom: 24px; +} + +.typography-brightness > span { + display: flex; + justify-content: space-between; + gap: 10px; +} + +.typography-brightness output { + color: #8aebd5; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; +} + +.typography-brightness input[type='range'] { + width: 100%; + accent-color: #63ddc1; +} + .slider-grid label { display: grid; gap: 9px; @@ -1993,6 +2290,7 @@ td:last-child { .totp-enrollment, .slider-grid, + .typography-settings, .two-columns { grid-template-columns: 1fr; } diff --git a/apps/overlay/src/control.tsx b/apps/overlay/src/control.tsx index e5d726b..e7efc61 100644 --- a/apps/overlay/src/control.tsx +++ b/apps/overlay/src/control.tsx @@ -34,6 +34,8 @@ import { PwaControls, usePwaUpdateBlocker } from './pwa' import { SongRequestOverlay } from './songOverlay' import { getOverlayTheme, overlayThemes } from './themes' import { currentLanguage, LanguageSelect, translate, useI18n } from './i18n' +import { fontFamilies } from './typography' +import type { FontFamilyId } from './typography' import { defaultGiftEffectSettings, defaultGiftMenuSettings, @@ -68,6 +70,51 @@ const previewPresets = [ { id: 'horizontal', labelKey: 'preview.horizontal', width: 720, height: 320 }, ] +function TypographySettingsFields({ + fontFamily, + fontBrightness, + onFontFamily, + onFontBrightness, +}: { + fontFamily: FontFamilyId + fontBrightness: number + onFontFamily: (font: FontFamilyId) => void + onFontBrightness: (brightness: number) => void +}) { + return ( +
+ + +
+ ) +} + function isDanmakuKind(kind: string): boolean { return kind === 'danmaku_overlay' || kind === 'danmaku' } @@ -227,6 +274,12 @@ function SettingsEditor({ {translate(selectedTheme.descriptionKey)} + edit('fontFamily', value)} + onFontBrightness={value => edit('fontBrightness', value)} + />
+ edit('fontFamily', value)} + onFontBrightness={value => edit('fontBrightness', value)} + />
+ edit('fontFamily', value)} + onFontBrightness={value => edit('fontBrightness', value)} + />
{translate('gift.settings.thresholds')}