打字机
我的个人博客和个人主页都使用了打字机,有一个很方便的库叫type.js,vue也有相关的vuetype.js库,但是我在使用一些布局时,这个库不能满足我的布局需要,所以我就自己写了一个;刚开始的思路是使用css+css变量,到后来发现很难控制,最后才用的js,这也算是一种思路的转变吧。
为什么使用js实现打字机,而不是用css
使用css动画 + css变量打字机
刚开始考虑使用css + css变量 来实现打字机,因为不喜欢使用定时器,大致思路就是使用js判断需要打印的字体个数,通过计算字体的数量来计算打印动画播放时长,以及分几步走,在一个时长内将这个元素的宽度从0一帧一帧变为自己的宽度,并且将这些计算好的属性传给css变量,从而实现打字的效果。这个动画不好的地方就在于字的宽度不好计算,字体大小不同算出来就不一样,而且字数少了或者多了打印出来总是会有一些字体出现的不完整,所以在需要自定义句子的时候不建议使用这种方法,当句子比较固定、只有一句的时候,可以使用。
css变量
css + css变量
.type-writer {
width: 0;
overflow: hidden;
color: #fffcec;
font-size: 1em;
cursor: pointer;
white-space: nowrap;
// --duration 表示总的播放时长,是根据打印字数的长度、每个字打印的时间计算的
// --length 表示字体长度,也就是多少帧
// --delay 表示打印多句话时每一句话之间间隔时长
animation: typing var(--duration) steps(var(--length)) var(--delay) forwards;
}
@keyframes typing {
0% {
width: 0;
}
100% {
width: var(--width);
}
}
// 打字机后面的 | 动画
// 使用after伪元素造 | 有个问题,当元素换行以后,位置会在这个元素第一行最大宽度的地方,而不是在换行结束的字后面 需要换行的时候慎用
.type-writer::after {
content: "|";
position: absolute;
right: -2px;
top: 1px;
height: 30px;
border-right: 1px solid #000;
animation: showInfinite 0.8s infinite both;
}
@keyframes showInfinite {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
使用js
js的原理是根据字数,一个字一个字使用setInterval打印出来
vue3版本
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
const props = defineProps({
// 打印字体列表 Array<string>
typeList: {
type: Array,
default: () => [],
},
// 字体大小 string
size: {
type: String,
default: "1rem",
},
// 句子与句子之间的间隔时间 number
timeSpace: {
type: Number,
default: 0.8,
},
// 打印一个字的时间长度 number
wordPrintTime: {
type: Number,
default: 0.3,
},
});
const loopList = ref([]); // 循环列表
const arr = []; // 收集定时器,组件销毁时清空
onMounted(() => {
// 没有就不打印
if (!props.typeList.length) return;
// 上一个打字机所用的时间,用于延迟开启下一个打字机打字
let lastTime = 0;
props.typeList.forEach((v, index) => {
if (!v.length) {
console.error(`第${index + 1}条语句为空,不能打印`);
return;
}
if (v.length < 3) {
console.error(`第${index + 1}条语句字数太少,最少三个字`);
return;
}
let loop = {
target: v,
delay: lastTime,
};
loopList.value.push(loop);
// 计算这一句播放的时间,用于下一句的播放 就是上一句的播放时长 + 自己本身需要播放的时间长度 + 句子与句子之间播放时间间隔
lastTime = Math.round((lastTime + v.length * props.wordPrintTime + props.timeSpace) * 10) / 10;
});
// 循环播放
loopList.value.forEach((loop) => {
// 使用定时器设置播放句子的时间 使得一句播放完了再播放下一句
let timer = setTimeout(() => {
// 这里可以使用ref避免一个页面重复出现id为writer,从而导致打印有问题的情况
const writers = document.getElementById("writer");
let num = 0,
str = "";
// 循环打字
let interTimer = setInterval(() => {
str += loop.target.charAt(num); // 一个字一个字地加
writers.innerHTML = str; // 将元素内部换成要打印的字
if (num < loop.target.length) {
num++;
} else {
// 打印结束清空定时器
clearInterval(interTimer);
interTimer = null;
}
}, props.wordPrintTime * 1000); // 每隔一定时间打印一个字
}, loop.delay * 1000); // 多少时间以后播放下一句
arr.push(timer);
});
});
// 播放结束,清空数据
onBeforeUnmount(() => {
arr.length &&
arr.forEach((a) => {
clearTimeout(a);
});
});
</script>
<template>
<div class="type-writer">
<span id="writer" :style="{ fontSize: size }"></span>
<!-- 为了元素换行 | 也会跟在后面,这里使用了行内元素 -->
<span class="space" :style="{ fontSize: size }">|</span>
</div>
</template>
<style lang="scss" scoped>
.type-writer {
color: #fffcec;
font-size: 1em;
cursor: pointer;
}
.space {
vertical-align: text-bottom;
animation: showInfinite 0.8s infinite both;
}
@keyframes showInfinite {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
react + ts版本
import { useEffect } from "react";
import classes from "./index.module.scss";
import { typeWriterProps } from "@/type/component.type";
export default function TypeWriter({ id = "writer", writerList = [], timeSpace = 0.8, wordPrintTime = 0.3, size = "1em", color }: typeWriterProps) {
// 收集定时器
let arr: Array<NodeJS.Timeout> = [];
useEffect(() => {
if (!writerList.length) return;
// 当打印的列表变化了,就开始打印
print();
}, [writerList]);
const print = () => {
let lastTime = 0; // 上一句播放的时间,用于开启下一句播放
let loopList = [] as Array<any>;
writerList.forEach((v, index) => {
if (!v.length) {
console.error(`第${index + 1}条语句为空,不能打印`);
return;
}
if (v.length < 3) {
console.error(`第${index + 1}条语句字数太少,最少三个字`);
return;
}
let loop = {
target: v,
delay: lastTime,
};
loopList.push(loop);
// 计算这一句播放的时间,用于下一句的播放 就是上一句的播放时长 + 自己本身需要播放的时间长度 + 句子与句子之间播放时间间隔
lastTime = Math.round((lastTime + v.length * wordPrintTime + timeSpace) * 10) / 10;
});
loopList.forEach((loop, index) => {
let timer: NodeJS.Timeout = setTimeout(() => {
// 这里可以使用ref避免一个页面重复出现id为writer,从而导致打印有问题的情况,也可以使用自定义的唯一id
const writers = document.getElementById(id);
let num = 0,
str = "";
let interTimer: any = setInterval(() => {
str += loop.target.charAt(num);// 一个字一个字地加
writers!.innerHTML = str; // 将元素内部换成要打印的字
if (num < loop.target.length) {
num++;
} else {
// 打印完了清空定时器
clearInterval(interTimer);
interTimer = null;
}
}, wordPrintTime * 1000);
if (index == loopList.length - 1) {
arr.forEach((v) => {
clearTimeout(v);
});
}
}, loop.delay * 1000);
arr.push(timer);
});
};
return (
<>
<div style={{ color, fontSize: size }} className={classes.typeWriter}>
<span id={id}></span>
<span className={classes.space}>|</span>
</div>
</>
);
}
index.module.scss
.typeWriter {
cursor: pointer;
color: #000;
}
.space {
animation: showInfinite 0.8s infinite both;
}
@keyframes showInfinite {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}