给自己的个人网站加一个打字机吧

打字机

我的个人博客和个人主页都使用了打字机,有一个很方便的库叫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;
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值