带残影的血条:显示每次受击的伤害量
血条下降时,把原先的长度留作一道浅色残影,停留约半秒再收缩到新值,玩家从残影的长度就能读出这一击的伤害量。本文介绍这种伤害残影的原理,以及 React 血条动画的两种实现:使用 zzzUI 的 Gauge,或用两个 div 加上过渡延迟。
效果演示
Hull
点击「Take a hit」:填充条立即缩短,原来的位置上留下一道浅色残影,停顿片刻后收缩到新值。点击「Patch up」则方向相反:淡色残影先延伸到新值,填充条随后增长到该位置。数值低于四分之一时,血条变红,并呈呼吸式明暗变化。
使用 Gauge 组件
zzzUI 的 Gauge 已实现上述全部效果。它基于 Base UI 的 Meter,名称取自 GaugeLabel,读屏软件会朗读当前值及其范围。low 是变红的比例阈值;segments 把它分成若干格,适合表示护盾或充能次数。
<Gauge value={hull} max={120} low={0.25}> <GaugeLabel>Hull</GaugeLabel> <GaugeValue /></Gauge>伤害残影的原理
同一个槽里叠着宽度相同的两层:残影在后,填充条在前。两层的宽度变化使用同一个过渡,关键在于其中一层延迟启动。数值下降时,残影延迟,因此在新的填充条后面仍会短暂显示旧值;数值上升时,填充条延迟,残影先到达新值,填充条随后增长到该位置。
因此组件只需知道数值上一次的变化方向。把前一个值保存在 state 中,在渲染过程中比较(React 保存前次渲染信息的标准写法),这样既不需要 effect,也不会多一次绘制。
function HealthBar({ value, max }: { value: number; max: number }) { const share = value / max; // which way it last moved, kept with React's "previous render" pattern const [last, setLast] = useState(share); const [dropped, setDropped] = useState(true); if (share !== last) { setLast(share); setDropped(share < last); } const width = `${share * 100}%`; return ( <div className="bar" role="meter" aria-label="Health" aria-valuenow={value} aria-valuemin={0} aria-valuemax={max}> <div className={dropped ? "trail" : "trail gain"} style={{ width, transitionDelay: dropped ? "480ms" : "0s" }} /> <div className="fill" style={{ width, transitionDelay: dropped ? "0s" : "480ms" }} /> </div> );}.bar { position: relative; height: 12px; overflow: hidden; background: #000; }.trail, .fill { position: absolute; inset: 0 auto 0 0; transition: width 420ms cubic-bezier(0.2, 0.7, 0.2, 1);}.trail { background: #f5f5f0; } /* the hit: pale, behind the fill */.trail.gain { background: #cde31c66; } /* a heal: a faint lead */.fill { background: #cde31c; }@media (prefers-reduced-motion: reduce) { .trail, .fill { transition-delay: 0s !important; }}停顿多久合适
半秒左右最合适:既足以看清这一击的伤害量,又足够短,能在下一击到来之前让血条恢复准确。zzzUI 把这个时长定义为设计 token --zero-dur-hold(480 ms),两层的移动则使用慢速时长(420 ms)。开启「减少动态效果」时,停顿降为零,数值偏低时的呼吸式明暗变化也随之取消:数值依然准确,只是不再有残影停留。