构建分割文本动画

关于如何构建分割字母和单词动画的基础概述。

在这篇文章中,我想分享关于如何为 web 解决分割文本动画和交互的想法,这些想法是最小化、可访问的,并且可以在各种浏览器中工作。试试演示

演示

如果您更喜欢视频,这里是这篇文章的 YouTube 版本

概述

分割文本动画可能非常精彩。在这篇文章中,我们将仅触及动画潜力的表面,但它确实为构建奠定了基础。目标是逐步实现动画效果。默认情况下文本应该是可读的,动画效果构建在其之上。分割文本运动效果可能会变得奢华且可能具有破坏性,因此我们只会在用户允许运动的情况下操作 HTML 或应用运动样式。

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

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

以下是我们要实现的条件结果的预览

screenshot of the Chrome devtools with the Elements panel open and reduced motion set to 'reduce' and the h1 is shown unsplit
用户首选降低运动效果:文本清晰易读/未分割

如果用户首选降低运动效果,我们将保持 HTML 文档不变,并且不进行任何动画处理。如果允许运动效果,我们将继续将其分割成碎片。以下是 JavaScript 按字母分割文本后的 HTML 预览。

screenshot of the Chrome devtools with the Elements panel open and reduced motion set to 'reduce' and the h1 is shown unsplit
用户允许运动效果;文本分割成多个 <span> 元素

准备运动条件

便捷的可用 @media (prefers-reduced-motion: reduce) 媒体查询将在此项目中的 CSS 和 JavaScript 中使用。此媒体查询是我们决定是否分割文本的主要条件。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 启用 Nesting Draft 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 中。属性 split-by 从 JavaScript 中使用,用于查找元素并为字母或单词创建框。属性 letter-animationword-animation 从 CSS 中使用,用于定位元素子元素并应用转换和动画。

以下是演示这两个属性的 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 devtools 显示单词之间的间隙

过渡分割单词示例

在此过渡示例中,我再次使用悬停。由于效果最初会隐藏内容直到悬停,因此我确保仅当设备具有悬停功能时才应用交互和样式。

@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;
  }
}

结论

既然您知道我是如何做到的,您会怎么做呢?! 🙂

让我们多样化我们的方法,学习在 web 上构建的所有方法。创建一个 Codepen 或托管您自己的演示,在推特上发给我,我会将其添加到下面的社区混音部分。

来源

更多演示和灵感

社区混音