构建拆分文本动画

有关如何制作拆分字母和字词动画的基础概览。

在这篇文章中,我想分享一些想法,介绍如何以最少的代码、无障碍的方式解决 Web 上的拆分文本动画和互动问题,并且这些解决方案适用于各种浏览器。试用演示版

演示

如果您更喜欢视频,可以观看此帖子的 YouTube 版本:

概览

拆分文字动画效果非常出色。本文将仅介绍动画潜力的冰山一角,但它确实为进一步探索奠定了基础。目标是逐步实现动画效果。文字应默认可读,动画应在文字之上构建。拆分文本动画效果可能会过于夸张,甚至会干扰用户,因此我们只有在用户同意使用动画时才会操纵 HTML 或应用动画样式。

以下是工作流程和结果的总体概览:

  1. 为 CSS 和 JS 准备精简的运动条件变量。
  2. 在 JavaScript 中准备拆分文本实用程序。
  3. 在网页加载时编排条件和实用程序。
  4. 为字母和字词编写 CSS 过渡和动画(精彩部分!)。

以下是我们希望实现的条件结果的预览:

Chrome Devtools 的屏幕截图,其中“元素”面板已打开,并将减少动画设置为“减少”,并且 h1 显示为未拆分
用户偏好减少运动:文本清晰可读 / 未拆分

如果用户偏好减少动态效果,我们会保持 HTML 文档不变,不执行任何动画。如果动作没问题,我们就继续将其剪辑成多个片段。以下是 JavaScript 按字母拆分文本后的 HTML 预览。

Chrome Devtools 的屏幕截图,其中“元素”面板已打开,并将减少动画设置为“减少”,并且 h1 显示为未拆分
用户可以接受动态效果;文字拆分为多个 <span> 元素

准备运动条件

此项目将使用 CSS 和 JavaScript 中方便可用@media (prefers-reduced-motion: reduce) 媒体查询。此媒体查询是我们决定是否拆分文本的主要条件。CSS 媒体查询将用于阻止过渡和动画,而 JavaScript 媒体查询将用于阻止 HTML 操作。

准备 CSS 条件

我使用 PostCSS 启用了 Media Queries Level 5 的语法,这样我就可以将媒体查询布尔值存储到变量中:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

准备 JS 条件

在 JavaScript 中,浏览器提供了一种检查媒体查询的方法,我使用解构从媒体查询检查中提取布尔值结果并对其进行重命名:

const {matches:motionOK} = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

然后,我可以测试 motionOK,并且仅在用户未请求减少运动时更改文档。

if (motionOK) {
  // document split manipulations
}

我可以使用 PostCSS 启用嵌套草稿 1 中的 @nest 语法,从而检查相同的值。这样一来,我就可以将有关父级和子级的动画及其样式要求的所有逻辑存储在一个位置:

letter-animation {
  @media (--motionOK) {
    /* animation styles */
  }
}

借助 PostCSS 自定义属性和 JavaScript 布尔值,我们就可以有条件地升级效果了。接下来,我们进入下一部分,我将在其中详细介绍用于将字符串转换为元素的 JavaScript。

拆分文本

无法使用 CSS 或 JS 为文字字母、字词、行等单独添加动画效果。 为了实现这种效果,我们需要使用盒子。如果我们想为每个字母添加动画效果,那么每个字母都需要是一个元素。如果我们想为每个字词添加动画效果,那么每个字词都需要是一个元素。

  1. 创建用于将字符串拆分为元素的 JavaScript 实用函数
  2. 协调这些实用程序的用法

拆分字母实用函数

不妨先从一个函数开始,该函数接受一个字符串,并返回一个包含每个字母的数组。

export const byLetter = text =>
  [...text].map(span)

ES6 中的扩展语法确实有助于快速完成这项任务。

拆分字词实用函数

与拆分字母类似,此函数接受一个字符串,并以数组形式返回每个字词。

export const byWord = text =>
  text.split(' ').map(span)

JavaScript 字符串的 split() 方法可让我们指定在哪些字符处进行切片。 我传递了一个空格,表示单词之间有分隔。

创建了框实用函数

此效果需要为每个字母设置方框,我们在这些函数中看到,map() 是使用 span() 函数调用的。以下是 span() 函数。

const span = (text, index) => {
  const node = document.createElement('span')

  node.textContent = text
  node.style.setProperty('--index', index)

  return node
}

请务必注意,系统正在使用数组位置设置名为 --index 的自定义属性。为字母动画添加方框很棒,但添加可在 CSS 中使用的索引看似微不足道,却能带来巨大影响。在这些影响中,最值得注意的是错开。我们将能够使用 --index 来偏移动画,以实现交错效果。

实用程序总结

完成后的 splitting.js 模块:

const span = (text, index) => {
  const node = document.createElement('span')

  node.textContent = text
  node.style.setProperty('--index', index)

  return node
}

export const byLetter = text =>
  [...text].map(span)

export const byWord = text =>
  text.split(' ').map(span)

接下来是导入和使用这些 byLetter()byWord() 函数。

拆分编排

在拆分实用程序准备就绪后,将所有内容整合在一起意味着:

  1. 确定要拆分的元素
  2. 拆分它们并将文本替换为 HTML

之后,CSS 会接管并为元素 / 框添加动画效果。

查找元素

我选择使用属性和值来存储有关所需动画以及如何拆分文本的信息。我喜欢将这些声明性选项放入 HTML 中。JavaScript 使用属性 split-by 来查找元素并为字母或字词创建框。CSS 中使用 letter-animationword-animation 属性来定位元素子级并应用转换和动画。

以下是一个 HTML 示例,展示了这两个属性:

<h1 split-by="letter" letter-animation="breath">animated letters</h1>
<h1 split-by="word" word-animation="trampoline">hover the words</h1>

通过 JavaScript 查找元素

我使用了 CSS 选择器语法来查找具有指定属性的元素,以收集需要拆分文本的元素列表:

const splitTargets = document.querySelectorAll('[split-by]')

通过 CSS 查找元素

我还使用了 CSS 中的属性存在选择器,为所有字母动画设置了相同的基本样式。稍后,我们将使用该属性值添加更具体的样式,以实现所需效果。

letter-animation {
  @media (--motionOK) {
    /* animation styles */
  }
}

就地拆分文本

对于我们在 JavaScript 中找到的每个拆分目标,我们将根据属性的值拆分其文本,并将每个字符串映射到 <span>。然后,我们可以将元素的文本替换为我们创建的方框:

splitTargets.forEach(node => {
  const type = node.getAttribute('split-by')
  let nodes = null

  if (type === 'letter') {
    nodes = byLetter(node.innerText)
  }
  else if (type === 'word') {
    nodes = byWord(node.innerText)
  }

  if (nodes) {
    node.firstChild.replaceWith(...nodes)
  }
})

编排总结

index.js 正在完成:

import {byLetter, byWord} from './splitting.js'

const {matches:motionOK} = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

if (motionOK) {
  const splitTargets = document.querySelectorAll('[split-by]')

  splitTargets.forEach(node => {
    const type = node.getAttribute('split-by')
    let nodes = null

    if (type === 'letter')
      nodes = byLetter(node.innerText)
    else if (type === 'word')
      nodes = byWord(node.innerText)

    if (nodes)
      node.firstChild.replaceWith(...nodes)
  })
}

该 JavaScript 可以按以下英文进行解读:

  1. 导入一些辅助实用函数。
  2. 检查相应用户是否可以进行运动,如果不能,则不执行任何操作。
  3. 对于要拆分的每个元素。
    1. 根据客户的拆分方式拆分这些数据。
    2. 将文本替换为元素。

拆分动画和过渡

上述拆分文档操作刚刚解锁了许多使用 CSS 或 JavaScript 实现的潜在动画和效果。本文底部提供了一些链接,可帮助您发掘拆分潜力。

是时候展示您能用它做什么了!我将分享 4 个由 CSS 驱动的动画和过渡效果。🤓

拆分字母

作为拆分字母效果的基础,我发现以下 CSS 非常有用。我将所有过渡效果和动画都放在了动态媒体查询后面,然后为每个新的子字母 span 提供了一个 display 属性以及一个用于处理空格的样式:

[letter-animation] > span {
  display: inline-block;
  white-space: break-spaces;
}

空白区样式非常重要,这样布局引擎就不会折叠仅包含空格的 span。现在,我们来介绍有状态的有趣内容。

过渡拆分字母示例

此示例使用 CSS 过渡来实现拆分文本效果。对于过渡,我们需要引擎在其中进行动画处理的状态,我选择了三种状态:无悬停、悬停在句子中、悬停在字母上。

当用户将鼠标悬停在句子(即容器)上时,我会缩小所有子元素,就好像用户将它们推得更远一样。然后,当用户将鼠标悬停在某个字母上时,我将其移到前面。

@media (--motionOK) {
  [letter-animation="hover"] {
    &:hover > span {
      transform: scale(.75);
    }

    & > span {
      transition: transform .3s ease;
      cursor: pointer;

      &:hover {
        transform: scale(1.25);
      }
    }
  }
}

为分拆的字母添加动画效果示例

此示例使用预定义的 @keyframe 动画来无限次地为每个字母添加动画效果,并利用内嵌的自定义属性索引来创建交错效果。

@media (--motionOK) {
  [letter-animation="breath"] > span {
    animation:
      breath 1200ms ease
      calc(var(--index) * 100 * 1ms)
      infinite alternate;
  }
}

@keyframes breath {
  from {
    animation-timing-function: ease-out;
  }
  to {
    transform: translateY(-5px) scale(1.25);
    text-shadow: 0 0 25px var(--glow-color);
    animation-timing-function: ease-in-out;
  }
}

拆分字词

在这些示例中,Flexbox 作为容器类型发挥了作用,很好地利用了 ch 单位作为合适的间距长度。

word-animation {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 1ch;
}
显示字词之间间距的 Flexbox 开发者工具

过渡分词示例

在此过渡效果示例中,我再次使用了悬停效果。由于该效果最初会隐藏内容,直到用户悬停时才显示,因此我确保只有在设备能够悬停的情况下才会应用互动和样式。

@media (hover) {
  [word-animation="hover"] {
    overflow: hidden;
    overflow: clip;

    & > span {
      transition: transform .3s ease;
      cursor: pointer;

      &:not(:hover) {
        transform: translateY(50%);
      }
    }
  }
}

动画效果拆分字词示例

在此动画示例中,我再次使用 CSS @keyframes 在常规段落文本上创建交错的无限动画。

[word-animation="trampoline"] > span {
  display: inline-block;
  transform: translateY(100%);
  animation:
    trampoline 3s ease
    calc(var(--index) * 150 * 1ms)
    infinite alternate;
}

@keyframes trampoline {
  0% {
    transform: translateY(100%);
    animation-timing-function: ease-out;
  }
  50% {
    transform: translateY(0);
    animation-timing-function: ease-in;
  }
}

总结

现在您已经知道我是如何做到的,那么您会怎么做呢?🙂

让我们丰富方法,了解在网络上构建内容的所有方式。 创建 Codepen 或自行托管演示,通过 Twitter 私信发送给我,我会将其添加到下方的“社区混音”部分。

来源

更多演示和灵感

社区混音作品