新主题
This commit is contained in:
@@ -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<OverlaySettings>),
|
||||
themeId: normalizeThemeId(settings.themeId),
|
||||
fontFamily: normalizeFontFamilyId(settings.fontFamily),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +224,7 @@ export function normalizeSongRequestSettings(value: unknown): SongRequestSetting
|
||||
...defaultSongRequestSettings,
|
||||
...(settings as Partial<SongRequestSettings>),
|
||||
themeId: normalizeThemeId(settings.themeId),
|
||||
fontFamily: normalizeFontFamilyId(settings.fontFamily),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +238,7 @@ export function normalizeGiftEffectSettings(value: unknown): GiftEffectSettings
|
||||
...defaultGiftEffectSettings,
|
||||
...(settings as Partial<GiftEffectSettings>),
|
||||
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<GiftMenuSettings>),
|
||||
themeId: normalizeGiftMenuThemeId(settings.themeId),
|
||||
fontFamily: normalizeFontFamilyId(settings.fontFamily),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="field-grid typography-settings">
|
||||
<label>
|
||||
{translate('settings.font_family')}
|
||||
<select
|
||||
value={fontFamily}
|
||||
onChange={event => onFontFamily(event.target.value as FontFamilyId)}
|
||||
>
|
||||
{fontFamilies.map(font => (
|
||||
<option value={font.id} key={font.id}>
|
||||
{translate(font.nameKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<small>{translate('settings.font_family_description')}</small>
|
||||
</label>
|
||||
<label className="typography-brightness">
|
||||
<span>
|
||||
{translate('settings.font_brightness')} <output>{fontBrightness}%</output>
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="70"
|
||||
max="180"
|
||||
step="5"
|
||||
value={fontBrightness}
|
||||
onChange={event => onFontBrightness(+event.target.value)}
|
||||
/>
|
||||
<small>{translate('settings.font_brightness_description')}</small>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function isDanmakuKind(kind: string): boolean {
|
||||
return kind === 'danmaku_overlay' || kind === 'danmaku'
|
||||
}
|
||||
@@ -227,6 +274,12 @@ function SettingsEditor({
|
||||
<small>{translate(selectedTheme.descriptionKey)}</small>
|
||||
</label>
|
||||
</div>
|
||||
<TypographySettingsFields
|
||||
fontFamily={settings.fontFamily}
|
||||
fontBrightness={settings.fontBrightness}
|
||||
onFontFamily={value => edit('fontFamily', value)}
|
||||
onFontBrightness={value => edit('fontBrightness', value)}
|
||||
/>
|
||||
<div className="slider-grid">
|
||||
<label>
|
||||
<span>
|
||||
@@ -434,6 +487,12 @@ function SongRequestSettingsEditor({
|
||||
<small>{translate(getOverlayTheme(settings.themeId).descriptionKey)}</small>
|
||||
</label>
|
||||
</div>
|
||||
<TypographySettingsFields
|
||||
fontFamily={settings.fontFamily}
|
||||
fontBrightness={settings.fontBrightness}
|
||||
onFontFamily={value => edit('fontFamily', value)}
|
||||
onFontBrightness={value => edit('fontBrightness', value)}
|
||||
/>
|
||||
<div className="slider-grid">
|
||||
<label>
|
||||
<span>
|
||||
@@ -588,6 +647,12 @@ function GiftEffectSettingsEditor({
|
||||
<small>{translate(theme.descriptionKey)}</small>
|
||||
</label>
|
||||
</div>
|
||||
<TypographySettingsFields
|
||||
fontFamily={settings.fontFamily}
|
||||
fontBrightness={settings.fontBrightness}
|
||||
onFontFamily={value => edit('fontFamily', value)}
|
||||
onFontBrightness={value => edit('fontBrightness', value)}
|
||||
/>
|
||||
<fieldset className="limit-grid gift-thresholds">
|
||||
<legend>{translate('gift.settings.thresholds')}</legend>
|
||||
<label>
|
||||
@@ -904,6 +969,12 @@ function GiftMenuSettingsEditor({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<TypographySettingsFields
|
||||
fontFamily={settings.fontFamily}
|
||||
fontBrightness={settings.fontBrightness}
|
||||
onFontFamily={value => edit('fontFamily', value)}
|
||||
onFontBrightness={value => edit('fontBrightness', value)}
|
||||
/>
|
||||
{catalogError && <div className="notice error">{catalogError}</div>}
|
||||
|
||||
<fieldset className="gift-menu-builder">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--gift-cyan, #a9fff2);
|
||||
font-family: var(--component-font-family, 'Noto Serif SC', 'Songti SC', 'STSong', serif);
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -353,3 +354,440 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Moonlit Water */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* This theme intentionally has no scene-sized colour wash. Every visible
|
||||
pixel below belongs to an effect, allowing OBS to retain a true alpha
|
||||
background over games, cameras and artwork. */
|
||||
.gift-theme-moonlit-water {
|
||||
color: #eadfc3;
|
||||
font-family: var(--component-font-family, FangSong, STFangsong, 'Noto Serif SC', serif);
|
||||
}
|
||||
|
||||
.moonlit-whisper-copy,
|
||||
.moonlit-gift-whisper > small,
|
||||
.moonlit-ceremony-copy,
|
||||
.guard-copy {
|
||||
filter: brightness(var(--component-font-brightness, 1.3));
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .moonlit-gift-whisper {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: grid;
|
||||
min-width: min(82vw, 310px);
|
||||
grid-template-columns: 2.5em minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.65em;
|
||||
color: #eadfc3;
|
||||
font-size: clamp(14px, 1.15vw, 23px);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.045em;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 7px rgba(11, 40, 42, 0.82);
|
||||
transform: translate3d(18px, 0, 0);
|
||||
animation: moonlit-whisper-appear 7.2s ease-in-out var(--moonlit-whisper-delay) both;
|
||||
}
|
||||
|
||||
.moonlit-whisper-ripple {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -0.35em;
|
||||
left: 2.7em;
|
||||
height: 1px;
|
||||
opacity: 0.65;
|
||||
background: linear-gradient(90deg, transparent, #e7c982 18% 72%, transparent);
|
||||
box-shadow: 0 0 7px rgba(231, 201, 130, 0.38);
|
||||
transform-origin: left;
|
||||
animation: moonlit-ripple-line 2.8s ease-out var(--moonlit-whisper-delay) both;
|
||||
}
|
||||
|
||||
.moonlit-whisper-image,
|
||||
.moonlit-ceremony-image {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.moonlit-whisper-image {
|
||||
width: 2.25em;
|
||||
height: 2.25em;
|
||||
}
|
||||
|
||||
.moonlit-whisper-image img,
|
||||
.moonlit-ceremony-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
filter: saturate(0.78) sepia(0.12) brightness(1.12) drop-shadow(0 0 8px rgba(231, 201, 130, 0.48));
|
||||
}
|
||||
|
||||
.moonlit-whisper-image .meteor-fallback,
|
||||
.moonlit-ceremony-image .meteor-fallback {
|
||||
color: #e7c982;
|
||||
font-size: 1.55em;
|
||||
text-shadow: 0 0 10px rgba(231, 201, 130, 0.7);
|
||||
}
|
||||
|
||||
.moonlit-whisper-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.08em;
|
||||
}
|
||||
|
||||
.moonlit-whisper-copy b {
|
||||
overflow: hidden;
|
||||
color: #b9caaa;
|
||||
font-size: 0.76em;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.moonlit-whisper-copy span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.moonlit-gift-whisper small {
|
||||
align-self: end;
|
||||
color: #e7c982;
|
||||
font-size: 0.75em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .moonlit-gift-ceremony {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 12;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
color: #eadfc3;
|
||||
isolation: isolate;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
animation: moonlit-offering-sweep 8.4s ease-in-out both;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-moon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: -3;
|
||||
width: min(44vmin, 560px);
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
opacity: 0.76;
|
||||
background:
|
||||
radial-gradient(circle at 35% 30%, rgba(255, 250, 226, 0.8), transparent 6%),
|
||||
radial-gradient(circle at 68% 43%, rgba(180, 194, 168, 0.24), transparent 12%),
|
||||
radial-gradient(circle at 42% 72%, rgba(172, 186, 159, 0.2), transparent 9%),
|
||||
radial-gradient(
|
||||
circle,
|
||||
#f4e8c8 0 43%,
|
||||
#d9d5b7 64%,
|
||||
rgba(212, 218, 188, 0.12) 71%,
|
||||
transparent 72%
|
||||
);
|
||||
box-shadow: 0 0 72px rgba(239, 224, 180, 0.23);
|
||||
transform: translate(-50%, -50%) scale(0.25);
|
||||
animation: moonlit-moon-rise 8.4s cubic-bezier(0.2, 0.78, 0.25, 1) both;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-water {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 7%;
|
||||
left: 0;
|
||||
z-index: -2;
|
||||
height: 30%;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-water i {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: min(60vw, 1040px);
|
||||
height: clamp(14px, 2.1vmin, 28px);
|
||||
border-top: 1px solid rgba(234, 223, 195, 0.6);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 -2px 16px rgba(154, 187, 167, 0.26);
|
||||
transform: translateX(-50%) scaleX(0.12);
|
||||
animation: moonlit-water-spread 5.5s ease-out both;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-water i:nth-child(1) {
|
||||
bottom: 5%;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-water i:nth-child(2) {
|
||||
bottom: 25%;
|
||||
width: min(47vw, 780px);
|
||||
animation-delay: 0.42s;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-water i:nth-child(3) {
|
||||
bottom: 45%;
|
||||
width: min(33vw, 560px);
|
||||
animation-delay: 0.72s;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-stars,
|
||||
.moonlit-ceremony-stars i {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-stars i {
|
||||
inset: auto;
|
||||
width: var(--moonlit-size);
|
||||
height: var(--moonlit-size);
|
||||
opacity: 0;
|
||||
background: #eadfc3;
|
||||
clip-path: polygon(50% 0, 59% 41%, 100% 50%, 59% 59%, 50% 100%, 41% 59%, 0 50%, 41% 41%);
|
||||
filter: drop-shadow(0 0 5px rgba(231, 201, 130, 0.78));
|
||||
animation: moonlit-ceremony-sparkle 2.4s ease-in-out var(--moonlit-delay) infinite;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-copy {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
max-width: min(80vw, 940px);
|
||||
justify-items: center;
|
||||
gap: clamp(5px, 0.9vmin, 12px);
|
||||
padding-bottom: min(17vmin, 180px);
|
||||
text-align: center;
|
||||
text-shadow: 0 2px 11px rgba(10, 38, 40, 0.88);
|
||||
}
|
||||
|
||||
.moonlit-ceremony-copy span {
|
||||
color: #b9caaa;
|
||||
font-size: clamp(12px, 1.35vw, 25px);
|
||||
letter-spacing: 0.5em;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-copy b {
|
||||
color: #e7c982;
|
||||
font-size: clamp(18px, 2.15vw, 41px);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-copy strong {
|
||||
color: #eadfc3;
|
||||
font-size: clamp(27px, 4.5vw, 84px);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.08em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-copy small {
|
||||
color: #e7c982;
|
||||
font-size: clamp(13px, 1.55vw, 29px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.moonlit-ceremony-image {
|
||||
position: absolute;
|
||||
bottom: max(7%, 4vmin);
|
||||
left: 50%;
|
||||
z-index: 3;
|
||||
width: min(14vmin, 176px);
|
||||
aspect-ratio: 1;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.moonlit-ceremony-image::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -22%;
|
||||
z-index: -1;
|
||||
border: 1px solid rgba(231, 201, 130, 0.7);
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 0 26px rgba(231, 201, 130, 0.32),
|
||||
inset 0 0 18px rgba(196, 219, 196, 0.16);
|
||||
animation: moonlit-image-halo 2.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Reuse the membership DOM but make its backdrop transparent and turn the
|
||||
generic nebula into a quiet, full-scene moon-and-water celebration. */
|
||||
.gift-theme-moonlit-water .guard-celebration {
|
||||
color: #eadfc3;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .guard-celebration::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: min(62vmin, 780px);
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(246, 237, 207, 0.82) 0 42%,
|
||||
rgba(226, 224, 193, 0.2) 63%,
|
||||
transparent 71%
|
||||
);
|
||||
box-shadow: 0 0 80px rgba(231, 201, 130, 0.24);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .guard-nebula {
|
||||
background:
|
||||
repeating-radial-gradient(
|
||||
ellipse at 50% 80%,
|
||||
transparent 0 7%,
|
||||
rgba(220, 225, 192, 0.2) 7.2% 7.45%,
|
||||
transparent 7.7% 12%
|
||||
),
|
||||
linear-gradient(90deg, transparent, rgba(231, 201, 130, 0.12), transparent);
|
||||
filter: blur(1px);
|
||||
animation: moonlit-guard-water 7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .guard-stars i {
|
||||
background: linear-gradient(135deg, #eadfc3, #e7c982 62%, #adc3ad);
|
||||
filter: drop-shadow(0 0 5px rgba(231, 201, 130, 0.72));
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .guard-halo {
|
||||
width: min(77vmin, 930px);
|
||||
border-color: rgba(231, 201, 130, 0.42);
|
||||
box-shadow:
|
||||
0 0 44px rgba(231, 201, 130, 0.18),
|
||||
inset 0 0 65px rgba(182, 207, 180, 0.1);
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .guard-halo i {
|
||||
border-color: rgba(203, 219, 184, 0.36);
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .guard-copy {
|
||||
text-shadow: 0 2px 13px rgba(8, 35, 38, 0.86);
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .guard-copy span,
|
||||
.gift-theme-moonlit-water .guard-copy b {
|
||||
color: #e7c982;
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water .guard-copy strong {
|
||||
color: #eadfc3;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.gift-theme-moonlit-water.gift-low-motion .moonlit-ceremony-moon,
|
||||
.gift-theme-moonlit-water.gift-low-motion .moonlit-ceremony-water i,
|
||||
.gift-theme-moonlit-water.gift-low-motion .moonlit-ceremony-stars,
|
||||
.gift-theme-moonlit-water.gift-low-motion .moonlit-ceremony-image::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes moonlit-whisper-appear {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate3d(18px, 0, 0);
|
||||
}
|
||||
12%,
|
||||
78% {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moonlit-ripple-line {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scaleX(0.15);
|
||||
}
|
||||
28%,
|
||||
78% {
|
||||
opacity: 0.7;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scaleX(1.18);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moonlit-offering-sweep {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
9%,
|
||||
85% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moonlit-moon-rise {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -40%) scale(0.2);
|
||||
}
|
||||
22%,
|
||||
78% {
|
||||
opacity: 0.76;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -55%) scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moonlit-water-spread {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) scaleX(0.12);
|
||||
}
|
||||
20%,
|
||||
76% {
|
||||
opacity: 0.8;
|
||||
transform: translateX(-50%) scaleX(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) scaleX(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moonlit-ceremony-sparkle {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.06;
|
||||
transform: scale(0.45) rotate(0);
|
||||
}
|
||||
46% {
|
||||
opacity: 0.95;
|
||||
transform: scale(1.12) rotate(42deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moonlit-image-halo {
|
||||
50% {
|
||||
opacity: 0.46;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moonlit-guard-water {
|
||||
50% {
|
||||
opacity: 0.68;
|
||||
transform: scale(1.04) translateY(-1.4%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { translate, useI18n } from './i18n'
|
||||
import type { ComponentStream } from './stream'
|
||||
import { defaultGiftEffectSettings } from './types'
|
||||
import type { GiftEffectSettings, MeteorTierSettings } from './types'
|
||||
import { typographyVariables } from './typography'
|
||||
|
||||
type Viewer = { uid?: string; name?: string }
|
||||
type Gift = {
|
||||
@@ -37,6 +38,9 @@ type VisualEffect = {
|
||||
type GiftTier = 'normal' | 'high' | 'featured'
|
||||
export type GiftEffectPreviewMode = GiftTier | 'guard'
|
||||
|
||||
/** The moonlit theme deliberately reserves a full-scene ceremony for CNY 50+. */
|
||||
const MOONLIT_CEREMONY_THRESHOLD = 50_000
|
||||
|
||||
function hash(value: string): number {
|
||||
let result = 2166136261
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
@@ -245,6 +249,95 @@ function MeteorBurst({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The small-gift treatment for the sparse moonlit theme. It is intentionally
|
||||
* compact and stays out of the centre of a broadcaster's scene: a gift image,
|
||||
* name and a single water-line appear briefly without adding a panel behind it.
|
||||
*/
|
||||
function MoonlitGiftWhisper({ effect }: { effect: VisualEffect }) {
|
||||
const gift = effect.payload.gift ?? {}
|
||||
const viewer = effect.payload.viewer?.name || translate('common.viewer')
|
||||
const quantity = effect.payload.quantity || 1
|
||||
const price = gift.priceCny ?? (gift.totalPrice ?? 0) / 1_000
|
||||
const seed = hash(effect.id)
|
||||
return (
|
||||
<article
|
||||
className="moonlit-gift-whisper"
|
||||
aria-label={gift.name || translate('common.gift')}
|
||||
style={
|
||||
{
|
||||
top: `${8 + (seed % 72)}%`,
|
||||
right: `${3 + ((seed >>> 8) % 10)}%`,
|
||||
['--moonlit-whisper-delay' as string]: `${(seed >>> 16) % 260}ms`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<i className="moonlit-whisper-ripple" aria-hidden="true" />
|
||||
<span className="moonlit-whisper-image">
|
||||
<GiftImage gift={gift} />
|
||||
</span>
|
||||
<div className="moonlit-whisper-copy">
|
||||
<b>{viewer}</b>
|
||||
<span>
|
||||
{translate('overlay.gift', { gift: gift.name || translate('common.gift'), quantity })}
|
||||
</span>
|
||||
</div>
|
||||
{price > 0 && <small>¥ {price.toFixed(2)}</small>}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A full-viewport but transparent offering ceremony for CNY 50+ gifts. The
|
||||
* CSS draws moon, water ripples and falling points of light, keeping the gift
|
||||
* image itself at the centre of the effect and leaving the OBS scene visible.
|
||||
*/
|
||||
function MoonlitGiftCeremony({ effect, tier }: { effect: VisualEffect; tier: GiftTier }) {
|
||||
const gift = effect.payload.gift ?? {}
|
||||
const viewer = effect.payload.viewer?.name || translate('common.viewer')
|
||||
const quantity = effect.payload.quantity || 1
|
||||
const price = gift.priceCny ?? (gift.totalPrice ?? 0) / 1_000
|
||||
return (
|
||||
<section className={`moonlit-gift-ceremony tier-${tier}`} aria-live="polite">
|
||||
<div className="moonlit-ceremony-moon" aria-hidden="true" />
|
||||
<div className="moonlit-ceremony-water" aria-hidden="true">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</div>
|
||||
<div className="moonlit-ceremony-stars" aria-hidden="true">
|
||||
{Array.from({ length: 14 }, (_, index) => {
|
||||
const seed = hash(`${effect.id}:ceremony:${index}`)
|
||||
return (
|
||||
<i
|
||||
key={index}
|
||||
style={
|
||||
{
|
||||
left: `${seed % 100}%`,
|
||||
top: `${(seed >>> 8) % 100}%`,
|
||||
['--moonlit-delay' as string]: `${-((seed >>> 16) % 2_400)}ms`,
|
||||
['--moonlit-size' as string]: `${4 + ((seed >>> 24) % 10)}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="moonlit-ceremony-copy">
|
||||
<span>{translate('gift.moonlit.ceremony_label')}</span>
|
||||
<b>{viewer}</b>
|
||||
<strong>
|
||||
{translate('overlay.gift', { gift: gift.name || translate('common.gift'), quantity })}
|
||||
</strong>
|
||||
{price > 0 && <small>¥ {price.toFixed(2)}</small>}
|
||||
</div>
|
||||
<div className="moonlit-ceremony-image">
|
||||
<GiftImage gift={gift} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function GuardCelebration({
|
||||
effect,
|
||||
settings,
|
||||
@@ -336,19 +429,32 @@ export function GiftEffectOverlay({
|
||||
|
||||
const scale = Math.min(1.5, Math.max(0.35, Math.min(bounds.width / 1920, bounds.height / 1080)))
|
||||
const guard = remote.effects.find(effect => effect.kind === 'guard')
|
||||
const moonlit = theme.id === 'moonlit-water'
|
||||
return (
|
||||
<main
|
||||
ref={root}
|
||||
className={`gift-effect-overlay ${theme.className} ${settings.lowPerformanceMode ? 'gift-low-motion' : ''}`}
|
||||
data-theme={theme.id}
|
||||
data-connection={stream?.connection || 'idle'}
|
||||
style={giftThemeVariables(theme)}
|
||||
style={{
|
||||
...giftThemeVariables(theme),
|
||||
...typographyVariables(settings.fontFamily, settings.fontBrightness),
|
||||
}}
|
||||
>
|
||||
<div className="meteor-sky" aria-live="polite">
|
||||
{remote.effects
|
||||
.filter(effect => effect.kind === 'gift')
|
||||
.map(effect => {
|
||||
const tier = tierFor(effect, settings)
|
||||
const gift = effect.payload.gift
|
||||
const totalPrice = gift?.totalPrice ?? Math.round((gift?.priceCny ?? 0) * 1_000)
|
||||
if (moonlit) {
|
||||
return totalPrice >= MOONLIT_CEREMONY_THRESHOLD ? (
|
||||
<MoonlitGiftCeremony effect={effect} tier={tier} key={effect.id} />
|
||||
) : (
|
||||
<MoonlitGiftWhisper effect={effect} key={effect.id} />
|
||||
)
|
||||
}
|
||||
return (
|
||||
<MeteorBurst
|
||||
effect={effect}
|
||||
|
||||
@@ -13,6 +13,7 @@ body,
|
||||
overflow: hidden;
|
||||
padding: clamp(4px, 1.2vmin, 14px);
|
||||
color: #eafffa;
|
||||
font-family: var(--component-font-family, 'Noto Serif SC', 'Songti SC', 'STSong', serif);
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -387,3 +388,143 @@ body,
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Moonlit Water is a transparent programme list rather than a framed menu.
|
||||
The thin rules are intentional typographic separators, not card borders. */
|
||||
.gift-menu-theme-moonlit-water.gift-menu-overlay {
|
||||
padding: clamp(8px, 1.3vmin, 16px);
|
||||
color: #eadfc3;
|
||||
font-family: var(--component-font-family, FangSong, STFangsong, 'Noto Serif SC', serif);
|
||||
}
|
||||
|
||||
.gift-menu-copy,
|
||||
.gift-menu-row > em,
|
||||
.gift-menu-empty {
|
||||
filter: brightness(var(--component-font-brightness, 1.3));
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-viewport {
|
||||
height: min(100%, var(--menu-panel-height));
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(231, 201, 130, 0.34);
|
||||
border-bottom: 1px solid rgba(231, 201, 130, 0.26);
|
||||
border-radius: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-row,
|
||||
.gift-menu-theme-moonlit-water .gift-menu-row:nth-child(3n + 2),
|
||||
.gift-menu-theme-moonlit-water .gift-menu-row:nth-child(3n) {
|
||||
gap: calc(var(--menu-row-height) * 0.12);
|
||||
padding-inline: calc(var(--menu-row-height) * 0.12);
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(231, 201, 130, 0.17);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-row::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-icon {
|
||||
width: calc(var(--menu-row-height) * 0.54);
|
||||
height: calc(var(--menu-row-height) * 0.54);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: #e7c982;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-gift-icon img {
|
||||
filter: saturate(0.75) sepia(0.1) brightness(1.08) drop-shadow(0 0 5px rgba(231, 201, 130, 0.4));
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-guard-icon img {
|
||||
filter: saturate(0.72) sepia(0.08) drop-shadow(0 0 5px rgba(231, 201, 130, 0.38));
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-battery-icon i {
|
||||
border-color: #e7c982;
|
||||
border-radius: 10%;
|
||||
background: linear-gradient(to top, rgba(231, 201, 130, 0.88) 0 68%, transparent 68%);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-battery-icon i::before {
|
||||
background: #e7c982;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-copy {
|
||||
gap: calc(var(--menu-row-height) * 0.025);
|
||||
text-shadow: 0 1px 6px rgba(10, 34, 38, 0.82);
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-copy strong {
|
||||
color: #e7c982;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.09em;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-copy span {
|
||||
color: #eadfc3;
|
||||
text-shadow: 0 1px 5px rgba(10, 34, 38, 0.86);
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-row em {
|
||||
inset: 0;
|
||||
padding: 0.28em 1em;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(231, 201, 130, 0.82);
|
||||
border-radius: 0;
|
||||
color: #eadfc3;
|
||||
background: linear-gradient(90deg, transparent, rgba(72, 105, 98, 0.32) 50%, transparent);
|
||||
box-shadow: none;
|
||||
text-shadow: 0 1px 6px rgba(10, 34, 38, 0.9);
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-row.triggered {
|
||||
border-bottom-color: rgba(231, 201, 130, 0.9);
|
||||
animation: moonlit-menu-awaken var(--menu-highlight-duration) ease-out both;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-row.triggered::before {
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 14%,
|
||||
rgba(231, 201, 130, 0.15) 42%,
|
||||
rgba(244, 235, 205, 0.6) 50%,
|
||||
rgba(174, 199, 175, 0.16) 59%,
|
||||
transparent 85%
|
||||
);
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-row-particles i {
|
||||
background: #e7c982;
|
||||
}
|
||||
|
||||
.gift-menu-theme-moonlit-water .gift-menu-empty {
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(231, 201, 130, 0.25);
|
||||
border-radius: 0;
|
||||
color: #afc2ad;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@keyframes moonlit-menu-awaken {
|
||||
0% {
|
||||
filter: brightness(1);
|
||||
}
|
||||
14% {
|
||||
filter: brightness(1.32);
|
||||
}
|
||||
100% {
|
||||
filter: brightness(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { translate, useI18n } from './i18n'
|
||||
import type { ComponentStream } from './stream'
|
||||
import { defaultGiftMenuSettings } from './types'
|
||||
import type { GiftMenuItem, GiftMenuSettings } from './types'
|
||||
import { typographyVariables } from './typography'
|
||||
|
||||
type MenuEnvelope = {
|
||||
id: string
|
||||
@@ -265,6 +266,7 @@ export function GiftMenuOverlay({
|
||||
style={
|
||||
{
|
||||
...giftMenuThemeVariables(theme),
|
||||
...typographyVariables(settings.fontFamily, settings.fontBrightness),
|
||||
['--menu-row-height' as string]: `${settings.rowHeight}px`,
|
||||
['--menu-panel-height' as string]: `${settings.rowHeight * settings.visibleRows}px`,
|
||||
['--menu-title-size' as string]: `${settings.rowHeight * 0.205 * (settings.fontScale / 100)}px`,
|
||||
|
||||
@@ -35,6 +35,24 @@ export const giftMenuThemes: readonly GiftMenuTheme[] = [
|
||||
vine: '/assets/floral-vine.svg',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'moonlit-water',
|
||||
nameKey: 'gift_menu.theme.moonlit_water.name',
|
||||
descriptionKey: 'gift_menu.theme.moonlit_water.description',
|
||||
className: 'gift-menu-theme-moonlit-water',
|
||||
palette: {
|
||||
jade: '#9bb8aa',
|
||||
cyan: '#d9e3c9',
|
||||
gold: '#e7c982',
|
||||
rose: '#dbc6a0',
|
||||
ink: 'transparent',
|
||||
},
|
||||
ornaments: {
|
||||
divider: '/assets/floral-divider.svg',
|
||||
cluster: '/assets/floral-cluster.svg',
|
||||
vine: '/assets/floral-vine.svg',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export function getGiftMenuTheme(id: unknown): GiftMenuTheme {
|
||||
|
||||
@@ -39,6 +39,24 @@ export const giftEffectThemes: readonly GiftEffectTheme[] = [
|
||||
starPulse: 'gift-star-pulse',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'moonlit-water',
|
||||
nameKey: 'gift.theme.moonlit_water.name',
|
||||
descriptionKey: 'gift.theme.moonlit_water.description',
|
||||
className: 'gift-theme-moonlit-water',
|
||||
palette: {
|
||||
jade: '#8daea2',
|
||||
cyan: '#c6d8c6',
|
||||
gold: '#e7c982',
|
||||
rose: '#dbc6a0',
|
||||
night: 'transparent',
|
||||
},
|
||||
motion: {
|
||||
meteor: 'moonlit-offering-sweep',
|
||||
guardReveal: 'moonlit-guard-reveal',
|
||||
starPulse: 'moonlit-star-pulse',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export function getGiftEffectTheme(id: unknown): GiftEffectTheme {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getOverlayTheme, normalizeThemeId, themeCssVariables } from './themes'
|
||||
import type { OverlayThemeDefinition } from './themes'
|
||||
import { defaultOverlaySettings } from './types'
|
||||
import type { OverlaySettings } from './types'
|
||||
import { typographyVariables } from './typography'
|
||||
|
||||
type Envelope = {
|
||||
id: string
|
||||
@@ -26,7 +27,7 @@ type Envelope = {
|
||||
}
|
||||
|
||||
type LivePayload = {
|
||||
viewer?: { uid?: string; name?: string }
|
||||
viewer?: { uid?: string; name?: string; avatarUrl?: string }
|
||||
text?: string
|
||||
message?: string
|
||||
guardName?: string
|
||||
@@ -243,6 +244,29 @@ function DanmakuBody({ payload }: { payload: LivePayload }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ViewerAvatar({ viewer }: { viewer: NonNullable<LivePayload['viewer']> }) {
|
||||
const [failed, setFailed] = useState(false)
|
||||
const name = viewer.name?.trim() || translate('common.viewer')
|
||||
const initial = Array.from(name)[0] || '观'
|
||||
|
||||
return (
|
||||
<div className="viewer-avatar" aria-hidden="true">
|
||||
{viewer.avatarUrl && !failed ? (
|
||||
<img
|
||||
src={viewer.avatarUrl}
|
||||
alt=""
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
referrerPolicy="no-referrer"
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
) : (
|
||||
<span>{initial}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Card({
|
||||
item,
|
||||
settings,
|
||||
@@ -293,6 +317,7 @@ function Card({
|
||||
</div>
|
||||
)}
|
||||
<div className="copy">
|
||||
{isDanmaku && <ViewerAvatar viewer={viewer} />}
|
||||
<b>{viewer.name || translate('common.viewer')}</b>
|
||||
<span className={isDanmaku ? 'danmaku-content' : undefined}>{body}</span>
|
||||
{typeof gift?.priceCny === 'number' && gift.priceCny > 0 && (
|
||||
@@ -385,6 +410,7 @@ export function Overlay({ preview = false, previewSettings, stream }: OverlayPro
|
||||
style={
|
||||
{
|
||||
...themeCssVariables(theme),
|
||||
...typographyVariables(settings.fontFamily, settings.fontBrightness),
|
||||
['--motion' as string]: `${settings.motionIntensity / 100}`,
|
||||
['--unfold-duration' as string]: `${settings.unfoldDurationMs || defaultOverlaySettings.unfoldDurationMs}ms`,
|
||||
['--particle-duration' as string]: `${400000 / Math.min(300, Math.max(25, settings.particleSpeed || defaultOverlaySettings.particleSpeed))}ms`,
|
||||
|
||||
+173
-1
@@ -16,7 +16,7 @@ body,
|
||||
gap: clamp(4px, 0.8vmin, 8px);
|
||||
padding: clamp(4px, 0.8vmin, 9px);
|
||||
color: var(--theme-text);
|
||||
font-family: 'Noto Serif SC', 'Microsoft YaHei', sans-serif;
|
||||
font-family: var(--component-font-family, 'Noto Serif SC', 'Songti SC', 'STSong', serif);
|
||||
font-size: var(--song-font-size, clamp(12px, 2.4vmin, 24px));
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -77,6 +77,14 @@ body,
|
||||
gap: 0.12em;
|
||||
}
|
||||
|
||||
.song-current-copy,
|
||||
.song-score,
|
||||
.song-queue > header,
|
||||
.song-row,
|
||||
.song-queue-empty {
|
||||
filter: brightness(var(--component-font-brightness, 1.3));
|
||||
}
|
||||
|
||||
.song-current-copy small,
|
||||
.song-score small {
|
||||
color: var(--theme-compact-user);
|
||||
@@ -367,3 +375,167 @@ body,
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Moonlit Water keeps the queue as a quiet typeset list: no panel fill, no
|
||||
glass, and just enough gold rule-work to remain legible over an OBS scene. */
|
||||
.song-overlay.theme-moonlit-water {
|
||||
gap: clamp(5px, 0.7vmin, 9px);
|
||||
padding: clamp(8px, 1.4vmin, 16px);
|
||||
color: #eadfc3;
|
||||
font-family: var(--component-font-family, FangSong, STFangsong, 'Noto Serif SC', serif);
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-current,
|
||||
.theme-moonlit-water .song-queue {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-current::after,
|
||||
.theme-moonlit-water .song-queue::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-current {
|
||||
min-height: min(var(--song-current-min, 56px), 62px);
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: clamp(5px, 0.9vmin, 10px);
|
||||
padding: 0 0 clamp(6px, 0.9vmin, 10px);
|
||||
border-bottom: 1px solid rgba(231, 201, 130, 0.38);
|
||||
animation: moonlit-song-current 0.68s ease-out both;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-current-mark {
|
||||
width: 1.72em;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: #e7c982;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
font-size: 1.42em;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-current-mark::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 1.4em;
|
||||
height: 1px;
|
||||
margin-top: 0.1em;
|
||||
background: linear-gradient(90deg, transparent, #e7c982, transparent);
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-current-copy,
|
||||
.theme-moonlit-water .song-score {
|
||||
gap: 0.08em;
|
||||
text-shadow: 0 1px 6px rgba(10, 34, 38, 0.82);
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-current-copy small,
|
||||
.theme-moonlit-water .song-score small,
|
||||
.theme-moonlit-water .song-requester,
|
||||
.theme-moonlit-water .song-queue > header {
|
||||
color: #afc2ad;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-current-copy strong {
|
||||
color: #eadfc3;
|
||||
font-size: 1.18em;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 7px rgba(10, 34, 38, 0.8);
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-score b,
|
||||
.theme-moonlit-water .song-index {
|
||||
color: #e7c982;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-queue > header {
|
||||
padding: 0.32em 0;
|
||||
border-bottom: 1px solid rgba(231, 201, 130, 0.24);
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-queue > header b {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
color: #e7c982;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-queue-track {
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-row {
|
||||
gap: 0.5em;
|
||||
padding: 0.45em 0.12em;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(231, 201, 130, 0.17);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
animation: moonlit-song-row-insert 0.54s ease-out both;
|
||||
animation-delay: var(--song-row-delay, 0ms);
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-row::before,
|
||||
.theme-moonlit-water .song-row::after {
|
||||
width: 0.42em;
|
||||
height: 0.42em;
|
||||
background: #e7c982;
|
||||
filter: drop-shadow(0 0 3px rgba(231, 201, 130, 0.5));
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-row::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-row strong {
|
||||
color: #eadfc3;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 6px rgba(10, 34, 38, 0.74);
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-particle-layer {
|
||||
opacity: 0.36;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-particle.star {
|
||||
background: #e7c982;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.theme-moonlit-water .song-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%);
|
||||
}
|
||||
|
||||
@keyframes moonlit-song-current {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moonlit-song-row-insert {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getOverlayTheme, themeCssVariables } from './themes'
|
||||
import type { OverlayThemeDefinition } from './themes'
|
||||
import { defaultSongRequestSettings } from './types'
|
||||
import type { SongRequestItem, SongRequestSettings } from './types'
|
||||
import { typographyVariables } from './typography'
|
||||
|
||||
type QueueState = {
|
||||
initialized: boolean
|
||||
@@ -307,6 +308,7 @@ export function SongRequestOverlay({
|
||||
style={
|
||||
{
|
||||
...themeCssVariables(theme),
|
||||
...typographyVariables(settings.fontFamily, settings.fontBrightness),
|
||||
['--song-font-size' as string]: `${Math.min(22, Math.max(11, Math.min(bounds.width, bounds.height) * 0.024)) * (settings.fontScale / 100)}px`,
|
||||
['--song-current-min' as string]: `${Math.min(72, Math.max(48, bounds.height * 0.08))}px`,
|
||||
} as CSSProperties
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { jadeScrollTheme } from './jadeScroll'
|
||||
import { moonlitWaterTheme } from './moonlitWater'
|
||||
import type { OverlayThemeDefinition, OverlayThemeId } from './types'
|
||||
|
||||
export type { OverlayThemeDefinition, OverlayThemeId, ThemeParticle } from './types'
|
||||
|
||||
/** Ordered registry used by both the control select and OBS renderer. */
|
||||
export const overlayThemes: readonly OverlayThemeDefinition[] = [jadeScrollTheme]
|
||||
export const overlayThemes: readonly OverlayThemeDefinition[] = [jadeScrollTheme, moonlitWaterTheme]
|
||||
|
||||
const themesById = new Map(overlayThemes.map(theme => [theme.id, theme]))
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { OverlayThemeDefinition } from './types'
|
||||
|
||||
const divider = '/assets/floral-divider.svg'
|
||||
const cluster = '/assets/floral-cluster.svg'
|
||||
const vine = '/assets/floral-vine.svg'
|
||||
|
||||
/**
|
||||
* A deliberately sparse, text-led theme inspired by moonlight on still water.
|
||||
*
|
||||
* Unlike the original glass theme, its surfaces are fully transparent: the
|
||||
* broadcaster's scene is the background and the overlay contributes only type,
|
||||
* fine rules, a few drifting petals, and a restrained moonlit shimmer.
|
||||
*/
|
||||
export const moonlitWaterTheme: OverlayThemeDefinition = {
|
||||
id: 'moonlit-water',
|
||||
nameKey: 'theme.moonlit_water.name',
|
||||
descriptionKey: 'theme.moonlit_water.description',
|
||||
className: 'theme-moonlit-water',
|
||||
palette: {
|
||||
text: '#eadfc3',
|
||||
user: '#c7d5b2',
|
||||
compactUser: '#a8beb0',
|
||||
accent: '#e7c982',
|
||||
price: '#f2d79c',
|
||||
},
|
||||
surfaces: {
|
||||
cardBorder: 'transparent',
|
||||
cardBackground: 'transparent',
|
||||
},
|
||||
ornaments: {
|
||||
variantCount: 4,
|
||||
particles: ['star', 'floret', 'star', 'floret', 'star', 'star'],
|
||||
patterns: { divider, cluster, vine },
|
||||
},
|
||||
motion: {
|
||||
arrive: 'moonlit-line-arrive',
|
||||
sheen: 'moonlit-line-sheen',
|
||||
featured: 'moonlit-gift-breathe',
|
||||
float: 'moonlit-drift',
|
||||
sparkle: 'moonlit-sparkle',
|
||||
unfurl: 'moonlit-reveal',
|
||||
railsOpen: 'moonlit-rule-reveal',
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Stable IDs persisted in component settings and emitted over WebSocket. */
|
||||
export const overlayThemeIds = ['jade-scroll'] as const
|
||||
export const overlayThemeIds = ['jade-scroll', 'moonlit-water'] as const
|
||||
|
||||
export type OverlayThemeId = (typeof overlayThemeIds)[number]
|
||||
export type ThemeParticle = 'star' | 'floret'
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
* server normally returns only `*Configured` flags after initial submission.
|
||||
*/
|
||||
import type { OverlayThemeId } from './themes'
|
||||
import type { FontFamilyId } from './typography'
|
||||
|
||||
export type OverlaySettings = {
|
||||
themeId: OverlayThemeId
|
||||
fontFamily: FontFamilyId
|
||||
fontBrightness: number
|
||||
fontScale: number
|
||||
showDanmaku: boolean
|
||||
showEnter: boolean
|
||||
@@ -30,6 +33,8 @@ export type OverlaySettings = {
|
||||
|
||||
export const defaultOverlaySettings: OverlaySettings = {
|
||||
themeId: 'jade-scroll',
|
||||
fontFamily: 'fang-song',
|
||||
fontBrightness: 130,
|
||||
fontScale: 140,
|
||||
showDanmaku: true,
|
||||
showEnter: true,
|
||||
@@ -52,6 +57,8 @@ export const defaultOverlaySettings: OverlaySettings = {
|
||||
/** Renderer and anti-spam settings for the built-in song request component. */
|
||||
export type SongRequestSettings = {
|
||||
themeId: OverlayThemeId
|
||||
fontFamily: FontFamilyId
|
||||
fontBrightness: number
|
||||
fontScale: number
|
||||
scrollSpeedPixelsPerSecond: number
|
||||
edgePauseSeconds: number
|
||||
@@ -63,6 +70,8 @@ export type SongRequestSettings = {
|
||||
|
||||
export const defaultSongRequestSettings: SongRequestSettings = {
|
||||
themeId: 'jade-scroll',
|
||||
fontFamily: 'fang-song',
|
||||
fontBrightness: 130,
|
||||
fontScale: 100,
|
||||
scrollSpeedPixelsPerSecond: 28,
|
||||
edgePauseSeconds: 2,
|
||||
@@ -71,7 +80,7 @@ export const defaultSongRequestSettings: SongRequestSettings = {
|
||||
requestCooldownSeconds: 0,
|
||||
}
|
||||
|
||||
export type GiftEffectThemeId = 'jade-starfall'
|
||||
export type GiftEffectThemeId = 'jade-starfall' | 'moonlit-water'
|
||||
|
||||
export type MeteorTierSettings = {
|
||||
count: number
|
||||
@@ -82,6 +91,8 @@ export type MeteorTierSettings = {
|
||||
/** Full-viewport visual settings for gift meteors and guard celebrations. */
|
||||
export type GiftEffectSettings = {
|
||||
themeId: GiftEffectThemeId
|
||||
fontFamily: FontFamilyId
|
||||
fontBrightness: number
|
||||
highValueThreshold: number
|
||||
featuredValueThreshold: number
|
||||
normal: MeteorTierSettings
|
||||
@@ -96,6 +107,8 @@ export type GiftEffectSettings = {
|
||||
|
||||
export const defaultGiftEffectSettings: GiftEffectSettings = {
|
||||
themeId: 'jade-starfall',
|
||||
fontFamily: 'fang-song',
|
||||
fontBrightness: 130,
|
||||
highValueThreshold: 10_000,
|
||||
featuredValueThreshold: 100_000,
|
||||
normal: { count: 3, size: 88, speed: 560 },
|
||||
@@ -135,10 +148,12 @@ export type GiftMenuItem = {
|
||||
description: string
|
||||
}
|
||||
|
||||
export type GiftMenuThemeId = 'jade-banquet'
|
||||
export type GiftMenuThemeId = 'jade-banquet' | 'moonlit-water'
|
||||
|
||||
export type GiftMenuSettings = {
|
||||
themeId: GiftMenuThemeId
|
||||
fontFamily: FontFamilyId
|
||||
fontBrightness: number
|
||||
items: GiftMenuItem[]
|
||||
visibleRows: number
|
||||
rowHeight: number
|
||||
@@ -151,6 +166,8 @@ export type GiftMenuSettings = {
|
||||
|
||||
export const defaultGiftMenuSettings: GiftMenuSettings = {
|
||||
themeId: 'jade-banquet',
|
||||
fontFamily: 'fang-song',
|
||||
fontBrightness: 130,
|
||||
items: [],
|
||||
visibleRows: 4,
|
||||
rowHeight: 88,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
/** Stable IDs persisted by the backend; each maps to a fixed, injection-safe stack. */
|
||||
export const fontFamilyIds = ['song', 'fang-song', 'kai'] as const
|
||||
|
||||
export type FontFamilyId = (typeof fontFamilyIds)[number]
|
||||
|
||||
export const fontFamilies: ReadonlyArray<{ id: FontFamilyId; nameKey: string }> = [
|
||||
{ id: 'song', nameKey: 'settings.font_family.song' },
|
||||
{ id: 'fang-song', nameKey: 'settings.font_family.fang_song' },
|
||||
{ id: 'kai', nameKey: 'settings.font_family.kai' },
|
||||
]
|
||||
|
||||
const fontStacks: Record<FontFamilyId, string> = {
|
||||
song: '"Source Han Serif SC", "Noto Serif CJK SC", "Noto Serif SC", "Songti SC", STSong, SimSun, serif',
|
||||
'fang-song': 'FangSong, STFangsong, "FZShuSong-Z01", "Noto Serif SC", "Songti SC", serif',
|
||||
kai: '"Kaiti SC", STKaiti, KaiTi, "LXGW WenKai", "Noto Serif SC", serif',
|
||||
}
|
||||
|
||||
export function normalizeFontFamilyId(value: unknown): FontFamilyId {
|
||||
return fontFamilyIds.includes(value as FontFamilyId) ? (value as FontFamilyId) : 'fang-song'
|
||||
}
|
||||
|
||||
export function typographyVariables(
|
||||
fontFamily: FontFamilyId,
|
||||
fontBrightness: number,
|
||||
): CSSProperties {
|
||||
const brightness = Math.min(180, Math.max(70, Number(fontBrightness) || 130))
|
||||
return {
|
||||
['--component-font-family' as string]: fontStacks[normalizeFontFamilyId(fontFamily)],
|
||||
['--component-font-brightness' as string]: `${brightness / 100}`,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user