在多行文本中逐字显示,且换行正确(按单词换行)

移动开发 2026-07-10

我正在开发一个只使用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: -->
  &#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;
  <!-- "é" precomposed: -->
  &eacute;
  <!-- e + combining acute: -->
  e&#x0301;
  <!-- "🇬🇧" regional indicators G+B: -->
  &#x1F1EC;&#x1F1E7;
  <!-- "👋🏽" wave + skin tone: -->
  &#x1F44B;&#x1F3FD;
  <!-- "1️⃣" digit 1 + VS16 + combining enclosing keycap -->
  1&#xFE0F;&#x20E3;
  <!-- 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&#x200C;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>

  &#x1F468;<span style="text-shadow: 0 0 3px; filter: blur(3px)">&#x200D;&#x1F469;&#x200D;&#x1F466;&#x200D;&#x1F467</span>

  &#x1F468;&#x200D;&#x1F469;<span style="text-shadow: 0 0 3px; filter: blur(3px)">&#x200D;&#x1F466;&#x200D;&#x1F467</span>

  &#x1F468;&#x200D;&#x1F469;&#x200D;<span style="text-shadow: 0 0 3px; filter: blur(3px)">&#x1F466;&#x200D;&#x1F467</span>

  &#x1F468;&#x200D;&#x1F469;&#x200D;<span style="text-shadow: 0 0 3px; filter: blur(3px)">&#x1F466;&#x200D;&#x1F467</span>

  &#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F466;&#x200D;&#x1F467

</p>

说明:

  • Firefox竟然也能逐步对复合字形簇的片段进行动画,即连字会逐步显现,甚至像 👨‍👩‍👧‍👦 这样的表情符号中的字形也会分阶段显示。Chrome会一次性对整个字形簇进行动画,因此在视觉上进入下一个字符的延迟会更大。不过我认为没必要排除这种情况。
  • 这个演示在Android版 Chrome上性能相当糟糕。(在Android版 Firefox上运行得相当好。)不过不可否认的是,它使用的花哨效果远超必要,而且比问题中的情境要复杂。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章