调整弹幕姬进入动态
This commit is contained in:
@@ -927,6 +927,13 @@ body,
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* In compact-only mode the whole bottom-anchored stack is moved with FLIP in
|
||||||
|
overlay.tsx. Suppress the per-card side entrance so the new row appears to
|
||||||
|
rise from below the component while the existing rows are pushed upward. */
|
||||||
|
.overlay.stack-arrival .card.danmaku.compact {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
.card.danmaku.expanded .copy,
|
.card.danmaku.expanded .copy,
|
||||||
.card.danmaku.compact .copy {
|
.card.danmaku.compact .copy {
|
||||||
padding-inline-end: clamp(38px, 8%, 58px);
|
padding-inline-end: clamp(38px, 8%, 58px);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* component settings, and malformed frames are ignored without terminating a
|
* component settings, and malformed frames are ignored without terminating a
|
||||||
* long-running browser source.
|
* long-running browser source.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
import type { ComponentStream } from './stream'
|
import type { ComponentStream } from './stream'
|
||||||
import { translate, useI18n } from './i18n'
|
import { translate, useI18n } from './i18n'
|
||||||
@@ -335,6 +335,9 @@ function Card({
|
|||||||
export function Overlay({ preview = false, previewSettings, stream }: OverlayProps) {
|
export function Overlay({ preview = false, previewSettings, stream }: OverlayProps) {
|
||||||
const { language } = useI18n()
|
const { language } = useI18n()
|
||||||
const root = useRef<HTMLDivElement>(null)
|
const root = useRef<HTMLDivElement>(null)
|
||||||
|
const cardsRef = useRef<HTMLDivElement>(null)
|
||||||
|
const previousCardTopsRef = useRef(new Map<string, number>())
|
||||||
|
const stackAnimationRef = useRef<Animation | null>(null)
|
||||||
const events = useEvents(preview, stream)
|
const events = useEvents(preview, stream)
|
||||||
const { items, setItems } = events
|
const { items, setItems } = events
|
||||||
const settings = previewSettings || events.settings
|
const settings = previewSettings || events.settings
|
||||||
@@ -408,10 +411,88 @@ export function Overlay({ preview = false, previewSettings, stream }: OverlayPro
|
|||||||
return () => window.clearTimeout(timer)
|
return () => window.clearTimeout(timer)
|
||||||
}, [items, settings.collapseAfterSeconds, settings.expandNewDanmaku, shape])
|
}, [items, settings.collapseAfterSeconds, settings.expandNewDanmaku, shape])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const cards = cardsRef.current
|
||||||
|
if (!cards) return
|
||||||
|
|
||||||
|
const elements = Array.from(cards.children).filter(
|
||||||
|
(element): element is HTMLElement => element instanceof HTMLElement,
|
||||||
|
)
|
||||||
|
const previousTops = previousCardTopsRef.current
|
||||||
|
const hasNewDanmaku = items.some(
|
||||||
|
item => item.type === 'live.danmaku' && !previousTops.has(item.key),
|
||||||
|
)
|
||||||
|
|
||||||
|
// If another push is still running, carry its current offset into the
|
||||||
|
// next FLIP animation so bursts of chat do not snap between positions.
|
||||||
|
let runningOffset = 0
|
||||||
|
const transform = getComputedStyle(cards).transform
|
||||||
|
if (transform !== 'none') {
|
||||||
|
try {
|
||||||
|
runningOffset = new DOMMatrixReadOnly(transform).m42
|
||||||
|
} catch {
|
||||||
|
runningOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stackAnimationRef.current?.cancel()
|
||||||
|
|
||||||
|
const currentTops = new Map<string, number>()
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const element = elements[index]
|
||||||
|
if (element) currentTops.set(item.key, element.getBoundingClientRect().top)
|
||||||
|
})
|
||||||
|
|
||||||
|
const reducedMotion =
|
||||||
|
settings.lowPerformanceMode ||
|
||||||
|
(window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false)
|
||||||
|
if (!settings.expandNewDanmaku && hasNewDanmaku && !reducedMotion && elements.length > 0) {
|
||||||
|
const anchorIndex = items.findIndex(item => previousTops.has(item.key))
|
||||||
|
const anchor = anchorIndex >= 0 ? elements[anchorIndex] : undefined
|
||||||
|
const anchorKey = anchorIndex >= 0 ? items[anchorIndex]?.key : undefined
|
||||||
|
const previousTop = anchorKey ? previousTops.get(anchorKey) : undefined
|
||||||
|
const currentTop = anchor?.getBoundingClientRect().top
|
||||||
|
const gap = Number.parseFloat(getComputedStyle(cards).rowGap) || 0
|
||||||
|
const fallbackOffset = cards.getBoundingClientRect().height + gap
|
||||||
|
const pushOffset = Math.max(
|
||||||
|
0,
|
||||||
|
previousTop !== undefined && currentTop !== undefined
|
||||||
|
? previousTop + runningOffset - currentTop
|
||||||
|
: fallbackOffset,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pushOffset > 0.5) {
|
||||||
|
const duration = 350 + settings.motionIntensity * 3.5
|
||||||
|
const animation = cards.animate(
|
||||||
|
[
|
||||||
|
{ transform: `translate3d(0, ${pushOffset}px, 0)` },
|
||||||
|
{ transform: 'translate3d(0, 0, 0)' },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration,
|
||||||
|
easing: 'cubic-bezier(0.2, 0.82, 0.24, 1)',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
stackAnimationRef.current = animation
|
||||||
|
animation.addEventListener('finish', () => {
|
||||||
|
if (stackAnimationRef.current === animation) stackAnimationRef.current = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousCardTopsRef.current = currentTops
|
||||||
|
}, [items, settings.expandNewDanmaku, settings.lowPerformanceMode, settings.motionIntensity])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
stackAnimationRef.current?.cancel()
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
ref={root}
|
ref={root}
|
||||||
className={`overlay ${theme.className} ${shape} ${settings.lowPerformanceMode ? 'low-motion' : ''}`}
|
className={`overlay ${theme.className} ${shape} ${settings.expandNewDanmaku ? '' : 'stack-arrival'} ${settings.lowPerformanceMode ? 'low-motion' : ''}`}
|
||||||
data-theme={theme.id}
|
data-theme={theme.id}
|
||||||
data-connection={stream?.connection || 'idle'}
|
data-connection={stream?.connection || 'idle'}
|
||||||
style={
|
style={
|
||||||
@@ -434,7 +515,7 @@ export function Overlay({ preview = false, previewSettings, stream }: OverlayPro
|
|||||||
>
|
>
|
||||||
<ThemeEdges />
|
<ThemeEdges />
|
||||||
<section className="wall">
|
<section className="wall">
|
||||||
<div className="cards">
|
<div className="cards" ref={cardsRef}>
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<Card
|
<Card
|
||||||
item={item}
|
item={item}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ WenKai 与漓雨手书。颜色覆盖同样只接受六位十六进制颜色;
|
|||||||
## 卡片生命周期
|
## 卡片生命周期
|
||||||
|
|
||||||
1. 新事件追加在可视区域底部;启用 `expandNewDanmaku`
|
1. 新事件追加在可视区域底部;启用 `expandNewDanmaku`
|
||||||
时,普通弹幕以卷轴动画横向展开,否则直接使用紧凑态。旧事件被向上顶出并裁切。
|
时,普通弹幕以卷轴动画横向展开。关闭时,新弹幕保持紧凑态并从组件底边外向上推入,同步顶起旧事件;超出顶部的旧事件会被裁切。
|
||||||
2. 用户名与内容在展开态分行显示,长内容完整换行。
|
2. 用户名与内容在展开态分行显示,长内容完整换行。
|
||||||
3. 新事件到达或超时后,旧卡变成紧凑态;内容不会隐藏。
|
3. 新事件到达或超时后,旧卡变成紧凑态;内容不会隐藏。
|
||||||
4. 紧凑态缩小字号并尽量压缩布局,但仍允许换行避免截断。
|
4. 紧凑态缩小字号并尽量压缩布局,但仍允许换行避免截断。
|
||||||
|
|||||||
Reference in New Issue
Block a user