新主题
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user