我看了Kevin Powell的视频,他用CSS重新制作了一个漂亮的类似打字机的动画。这很好,你一定要看一看,因为里面有真正的CSS技巧。我相信你已经看到了其他的CSS尝试,包括这个网站自己的片段。
像凯文一样,我决定重新制作这个动画,但把它开放给JavaScript。这样,我们就有了一些额外的工具,可以使打字的感觉更自然,甚至更有活力。许多CSS解决方案都依赖于基于文本长度的神奇数字,但通过JavaScript,我们可以制作一些能够接受我们扔给它的任何文本的东西。
所以,让我们来做这个。在本教程中,我将展示我们可以只通过改变实际的文本就能对多个单词进行动画。不需要在每次添加新词的时候修改代码,因为JavaScript会帮你做这件事
从文本开始
让我们从文本开始。我们使用单色字体来实现这个效果。为什么?因为在单行字体中,每个字符或字母所占的水平空间是相等的,当我们在制作文本的动画时使用steps() 的概念时,这将非常方便。当我们已经知道一个字符的确切宽度,并且所有字符都有相同的宽度时,事情就更容易预测了。
我们有三个元素放在一个容器里:一个元素用于实际的文本,一个用于隐藏文本,一个用于动画光标。
<div class="container">
<div class="text_hide"></div>
<div class="text">Typing Animation</div>
<div class="text_cursor"></div>
</div>
我们可以在这里使用::before 和::after 伪元素,但它们对JavaScript来说不是很好。伪元素不是DOM的一部分,而是用作CSS中元素样式的额外钩子。用真正的元素工作会更好。
我们将文本完全隐藏在.text_hide 元素后面。这很关键。这是一个空的div,它拉伸了文本的宽度,并将其挡住,直到动画开始--这时我们才开始看到文本从元素后面移动出来。
为了覆盖整个文本元素,将.text_hide 元素放在文本元素的顶部,高度和宽度与文本元素相同。记得将.text_hide 元素的background-color ,与文本周围的背景完全相同,这样一切都会融合在一起。
.container {
position: relative;
}
.text {
font-family: 'Roboto Mono', monospace;
font-size: 2rem;
}
.text_hide {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: white;
}
光标
接下来,让我们做一个小的光标,在输入文字的时候闪烁。我们先不讨论闪烁的部分,只关注光标本身。
让我们制作另一个元素,其类别为.text_cursor 。其属性将与.text_hide 元素相似,但有一点不同:我们不设置background-color ,而是保留background-color transparent (因为它在技术上是不必要的,然后在新的.text_cursor 元素的左边缘添加一个边界。
.text_cursor{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: transparent;
border-left: 3px solid black;
}
现在我们得到了一个看起来像光标的东西,它可以随着文字的移动而随时移动。
JavaScript动画
现在,超级有趣的部分来了--让我们用JavaScript把这些东西做成动画吧我们将首先把所有的东西包裹在一个叫做typing_animation() 的函数中。
function typing_animation(){
// code here
}
typing_animation();
下一个任务是使用split() 方法将文本的每一个字符存储在一个数组中。这样就把字符串分成了只有一个字符的子串,然后返回一个包含所有子串的数组。
function typing_animation(){
let text_element = document.querySelector(".text");
let text_array = text_element.innerHTML.split("");
}
例如,如果我们把 "打字动画 "作为一个字符串,那么输出的结果是:
我们还可以确定该字符串中的总字符数。为了只得到字符串中的单词,我们用split(" ") ,替换split("") 。请注意,这两者之间是有区别的。在这里," " 作为一个分隔符。每当我们遇到一个空格,它就会终止子串,并将其作为一个数组元素存储。然后,这个过程将持续到整个字符串。
function typing_animation(){
let text_element = document.querySelector(".text");
let text_array = text_element.innerHTML.split("");
let all_words = text_element.innerHTML.split(" ");
}
例如,对于一个字符串 "Typing Animation",输出结果将是:
现在,让我们来计算整个字符串的长度,以及每一个单独的单词的长度。
function typing_animation() {
let text_element = document.querySelector(".text");
let text_array = text_element.innerHTML.split("");
let all_words = text_element.innerHTML.split(" ");
let text_len = text_array.length;
const word_len = all_words.map((word) => {
return word.length;
});
}
为了得到整个字符串的长度,我们必须访问包含所有字符的单个元素的数组的长度。如果我们谈论的是单个单词的长度,那么我们可以使用map() 方法,该方法每次从all_words 数组中访问一个单词,然后将该单词的长度存储到一个新的数组中,称为word_len 。这两个数组的元素数量相同,但是一个数组包含_实际的单词_,另一个数组包含_单词的长度_。
现在我们可以做动画了!我们使用网络动画API,因为我们在这里使用的是纯JavaScript,在这个例子中没有CSS动画。
首先,让我们为光标制作动画。它需要无限地闪烁。我们需要关键帧和动画属性,它们都将被存储在各自的JavaScript对象中。这里是关键帧。
document.querySelector(".text_cursor").animate([
{
opacity: 0
},
{
opacity: 0, offset: 0.7
},
{
opacity: 1
}
], cursor_timings);
我们已经将三个关键帧定义为对象,存储在一个数组中。术语offset: 0.7 ,只是意味着在动画完成70%后,不透明度将从0过渡到1。
现在,我们必须定义动画属性。为此,让我们创建一个JavaScript对象,把它们放在一起。
let cursor_timings = {
duration: 700, // milliseconds (0.7 seconds)
iterations: Infinity, // number of times the animation will work
easing: 'cubic-bezier(0,.26,.44,.93)' // timing-function
}
我们可以给动画一个名字,就像这样:
let animation = document.querySelector(".text_cursor").animate([
// keyframes
], //properties);
很好!现在,让我们给.text_hide 元素做动画,就像它的名字一样,隐藏文本。我们为这个元素定义动画属性。
let timings = {
easing: `steps(${Number(word_len[0])}, end)`,
delay: 2000, // milliseconds
duration: 2000, // milliseconds
fill: 'forwards'
}
easing 属性定义了动画的速度将如何随时间变化。在这里,我们使用了steps() 计时函数。这使得元素的动画是不连续的,而不是平滑的连续动画--你知道,为了更自然的打字动作。例如,动画的持续时间是两秒,所以steps() 函数以9 的步骤("动画 "中每个字符一个步骤)使元素成为动画,持续两秒,其中每个步骤的持续时间为2/9 = 0.22 秒。
end 参数使元素保持其初始状态,直到第一步的持续时间结束。这个参数是可选的,其默认值被设置为end 。如果你想深入了解steps() ,那么你可以参考Joni Trythall的这篇精彩文章。
fill 属性与CSS中的animation-fill-mode 属性相同。通过设置它的值为forwards ,在动画完成后,元素将保持在最后一个关键帧所定义的相同位置。
接下来,我们将定义关键帧。
let reveal_animation_1 = document.querySelector(".text_hide").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0])}%` }
], timings);
现在我们只做一个词的动画。稍后,我们将看到如何为多个单词制作动画。
最后一个关键帧是至关重要的。比方说,我们想给 "Animation "这个词做动画。它的长度是9 (因为有九个字符),我们知道它被存储为一个变量,这要感谢我们的typing_animation() 函数。声明100/text_len ,结果是100/9 ,或11.11%,这是 "Animation "这个词中每一个字符的宽度。这意味着每一个字符的宽度都是整个单词宽度的11.11%。如果我们用这个值乘以第一个单词的长度(在我们的例子中是9 ),那么我们就得到了100%。是的,我们可以直接写100%,而不是做这些事情。但是当我们为多个单词制作动画时,这个逻辑将帮助我们。
所有这些的结果是,.text_hide 元素从left: 0% 到left: 100% 的动画。换句话说,这个元素的宽度随着它的移动从100%减少到0%。
我们也必须为.text_cursor 元素添加同样的动画,因为我们希望它能与.text_hide 元素一起从左到右过渡。
为多个词制作动画
假设我们有两个想要打出来的字,也许是 "打字动画"。我们按照上次的程序为第一个词制作动画。然而这一次,我们要改变动画属性中的缓和函数值。
let timings = {
easing: `steps(${Number(word_len[0] + 1)}, end)`,
delay: 2000,
duration: 2000,
fill: 'forwards'
}
我们把这个数字增加了一个步骤。为什么呢?好吧,一个词后面有一个空格怎么办?我们必须考虑到这一点。但是,如果一个句子中只有一个词怎么办?为此,我们将写一个if 条件,其中,如果字数等于1,那么steps(${Number(word_len[0])}, end) 。如果字数不等于1,则steps(${Number(word_len[0] + 1)}, end) 。
function typing_animation() {
let text_element = document.querySelector(".text");
let text_array = text_element.innerHTML.split("");
let all_words = text_element.innerHTML.split(" ");
let text_len = text_array.length;
const word_len = all_words.map((word) => {
return word.length;
})
let timings = {
easing: `steps(${Number(word_len[0])}, end)`,
delay: 2000,
duration: 2000,
fill: 'forwards'
}
let cursor_timings = {
duration: 700,
iterations: Infinity,
easing: 'cubic-bezier(0,.26,.44,.93)'
}
document.querySelector(".text_cursor").animate([
{
opacity: 0
},
{
opacity: 0, offset: 0.7
},
{
opacity: 1
}
], cursor_timings);
if (all_words.length == 1) {
timings.easing = `steps(${Number(word_len[0])}, end)`;
let reveal_animation_1 = document.querySelector(".text_hide").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0])}%` }
], timings);
document.querySelector(".text_cursor").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0])}%` }
], timings);
} else {
document.querySelector(".text_hide").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0] + 1)}%` }
], timings);
document.querySelector(".text_cursor").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0] + 1)}%` }
], timings);
}
}
typing_animation();
对于一个以上的单词,我们用一个for 循环来迭代和动画化第一个单词之后的每个单词。
for(let i = 1; i < all_words.length; i++){
// code
}
为什么我们要采取i = 1 ?因为当这个for 循环被执行时,第一个词已经被动画化了。
接下来,我们将访问各个单词的长度:
for(let i = 1; i < all_words.length; i++){
const single_word_len = word_len[i];
}
让我们也为第一个单词之后的所有单词定义动画属性:
// the following code goes inside the for loop
let timings_2 = {
easing: `steps(${Number(single_word_len + 1)}, end)`,
delay: (2 * (i + 1) + (2 * i)) * (1000),
duration: 2000,
fill: 'forwards'
}
这里最重要的是delay 属性。如你所知,对于第一个词,我们只是将delay 属性设置为两秒;但现在我们必须以动态方式增加第一个词之后的词的延迟。
第一个词的延迟是两秒。它的动画持续时间也是两秒,加起来一共是四秒。但是在第一个词和第二个词的动画之间应该有一些间隔,使动画更加真实。我们可以做的是在每个词之间添加两秒的延迟,而不是一秒。这使得第二个词的总延迟为2 + 2 + 2 ,即6秒。同样地,为第三个词制作动画的总延迟为10秒,以此类推。
这个模式的函数是这样的:
(2 * (i + 1) + (2 * i)) * (1000)
...这里我们要乘以1000来把秒转换为毫秒。
| 字的长度 | 一个字产生动画所需的时间 |
| 6 | 2/6 = 0.33秒 |
| 8 | 2/8 = 0.25秒 |
| 9 | 2/9 = 0.22秒 |
| 12 | 2/12 = 0.17秒 |
*总时间为2秒
这个词越长,它被揭示的速度就越快。为什么?因为无论这个词有多长,持续时间都是一样的。玩弄一下持续时间和延迟属性,让事情变得恰到好处。
还记得我们通过考虑到一个词后的一个空格来改变steps() 值吗?同样地,句子中的最后一个词后面没有空格,因此,我们应该在另一个if 语句中考虑到这一点。
// the following code goes inside the for loop
if (i == (all_words.length - 1)) {
timings_2.easing = `steps(${Number(single_word_len)}, end)`;
let reveal_animation_2 = document.querySelector(".text_hide").animate([
{ left: `${left_instance}%` },
{ left: `${left_instance + ((100 / text_len) * (word_len[i]))}%` }
], timings_2);
document.querySelector(".text_cursor").animate([
{ left: `${left_instance}%` },
{ left: `${left_instance + ((100 / text_len) * (word_len[i]))}%` }
], timings_2);
} else {
document.querySelector(".text_hide").animate([
{ left: `${left_instance}%` },
{ left: `${left_instance + ((100 / text_len) * (word_len[i] + 1))}%` }
], timings_2);
document.querySelector(".text_cursor").animate([
{ left: `${left_instance}%` },
{ left: `${left_instance + ((100 / text_len) * (word_len[i] + 1))}%` }
], timings_2);
}
那个left_instance 变量是什么?我们还没有讨论过它,然而它是我们正在做的事情中最关键的部分。让我解释一下。
0% 是第一个词的left 属性的初始值。但是,第二个词的初始值应该等于第一个词的_最终_ left 属性值。
if (i == 1) {
var left_instance = (100 / text_len) * (word_len[i - 1] + 1);
}
word_len[i - 1] + 1 是指前一个词的长度(包括一个空白)。
我们有两个词,"打字动画"。这使得text_len 等于16 ,这意味着每个字符是全宽的6.25% (100/text_len = 100/16),再乘以第一个字的长度,7 。所有这些数学运算给了我们43.75 ,事实上,这就是第一个字的宽度。换句话说,第一个词的宽度是43.75% ,是整个字符串的宽度。这意味着第二个词是从第一个词的位置开始动画的。
最后,让我们在for 循环的最后更新left_instance 变量。
left_instance = left_instance + ((100 / text_len) * (word_len[i] + 1));
现在,你可以在HTML中输入任意多的单词,而动画就可以_正常工作了_!
我们走吧:一个更强大的JavaScript版本的打字动画。超酷的是,CSS也有一种方法(甚至是多种方法)来做同样的事情。在特定情况下,CSS甚至可能是更好的方法。但是,当我们需要的增强功能超出了CSS所能处理的范围时,撒上一些JavaScript就能很好地解决这个问题。在这种情况下,我们增加了对所有单词的支持,无论它们包含多少个字符,以及对多个单词进行动画的能力。而且,在单词之间有一个小的额外延迟,我们得到一个超级自然的动画。
就这样了,希望你觉得这很有趣签字。