在多行文本中逐字显示,且换行正确(按单词换行)
我正在开发一个只使用HTML/CSS/JS的网页应用,文本会一个字符一个字符地显现。
我当前在同一个文本框里有两个元素,一个是“幽灵文本”,决定文本框的最终大小,另一个则通过JavaScript逐步显示。
我遇到的问题是在行末,文本经常会超过“幽灵文本”所占区域,突然换到下一行,看起来很突兀。

在保持字距(kerning)的前提下,如何防止这种现象发生?
我尝试过使用Pretext库,但由于这是一个单页应用且没有Node.js,无法使用它。
解决方案
下面是一个使用单个元素的简易POC(通过JS在客户端对文本进行拆分),因此“阴影文本”和“真实文本”之间没有分离,并附带一些可推荐的无障碍性最佳实践以及若干边界情况的改进:
document.querySelectorAll('.lettering').forEach(splitText);
function splitText(el) {
// Early NOOP for contenxt where it does make sense or would violate accessibility:
if (!CSS.supports('animation-name', 'x')) return;
// commented out FOR REMO PURPOSES ONLY (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
if (matchMedia('print').matches) return;
// sr-only clone for screen readers plus aria-hidden on the target el:
const clone = el.cloneNode(true);
clone.classList.remove('lettering');
clone.classList.add('sr-only');
clone.removeAttribute('id');
el.before(clone);
el.setAttribute('aria-hidden', "true")
// actual work:
let i = 0;
let lastWasSpace = false;
const whiteSpace = /[\t\n\r\f\v ]{1,}/g;
const nonPaints = /^[\p{Z}\p{M}\p{Cf}\p{RI}]$/u;
const text = el.textContent.replace(whiteSpace, ' ').trim().normalize('NFC');
function wrap(glyph) {
if (nonPaints.test(glyph) && !lastWasSpace) {
--i
}
lastWasSpace = ' ' === glyph;
return `<span style="--i: ${++i}">${glyph}</span>`;
}
el.innerHTML = [...text].map(wrap).join('');
}
p {
font-size: 28pt;
font-feature-settings: "liga" on;
width: 10em;
margin: 0;
}
@media (update: fast) {
.lettering span {
animation-name: fadein;
animation-duration: .5s;
animation-fill-mode: both;
animation-delay: calc(var(--i) * .1s);
filter: blur(.3em);
text-shadow: 0 0 .2em, 0 0 .4em, 0 0 .6em;
opacity: .5;
}
}
@keyframes fadein {
to {
text-shadow: none;
filter: none;
opacity: 1;
}
}
[type="reset"]:active~* span {
animation: none;
}
#slow:checked~.lettering span {
animation-duration: 3s;
animation-delay: calc(var(--i) * 1s)
}
#pause:checked~.lettering span {
animation-play-state: paused;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
html {
color-scheme: dark light;
background-color: #666;
font-family: 'Helvetica Neue', Corbel, Calibri, Segoe UI Emoji, 'DejaVu Sans', Roboto, sans-serif, twemoji-mozilla;
}
body > * {
background-color: color-mix(in srgb, canvas, transparent 30%);
color: canvastext;
}
<input type="reset">
<input type="checkbox" id="slow"><label for="slow">Slow</label>
<input type="checkbox" id="pause"><label for="pause">Pause</label>
<p class="lettering">
<!-- "👨👩👧👦" - man ZWJ woman ZWJ girl ZWJ boy: -->
👨‍👩‍👧‍👦
<!-- "é" precomposed: -->
é
<!-- e + combining acute: -->
é
<!-- "🇬🇧" regional indicators G+B: -->
🇬🇧
<!-- "👋🏽" wave + skin tone: -->
👋🏽
<!-- "1️⃣" digit 1 + VS16 + combining enclosing keycap -->
1️⃣
<!-- Ligatures -->
fl ffl fi
<!-- Prose -->
Sounds like itʼs related to “column”, but that was a historical misreading; really related to “collinear”.
</p>
<hr> Tangential tests
<p>
fi
f‌i
<span style="text-shadow: 0 0 3px; filter: blur(3px)">f</span><span>i</span>
<span>f</span><span style="text-shadow: 0 0 3px; filter: blur(3px)">i</span>
👨<span style="text-shadow: 0 0 3px; filter: blur(3px)">‍👩‍👦‍👧</span>
👨‍👩<span style="text-shadow: 0 0 3px; filter: blur(3px)">‍👦‍👧</span>
👨‍👩‍<span style="text-shadow: 0 0 3px; filter: blur(3px)">👦‍👧</span>
👨‍👩‍<span style="text-shadow: 0 0 3px; filter: blur(3px)">👦‍👧</span>
👨‍👩‍👦‍👧
</p>
说明:
- Firefox竟然也能逐步对复合字形簇的片段进行动画,即连字会逐步显现,甚至像 👨👩👧👦 这样的表情符号中的字形也会分阶段显示。Chrome会一次性对整个字形簇进行动画,因此在视觉上进入下一个字符的延迟会更大。不过我认为没必要排除这种情况。
- 这个演示在Android版 Chrome上性能相当糟糕。(在Android版 Firefox上运行得相当好。)不过不可否认的是,它使用的花哨效果远超必要,而且比问题中的情境要复杂。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。