前端炫酷动画--文字(二)

目录

一、弧形边框选项卡

二、零宽字符

三、目录滚动时自动高亮

四、高亮关键字

五、文字描边

六、按钮边框的旋转动画

七、视频文字特效

八、立体文字特效+让文字立起来

九、文字连续光影特效

十、重复渐变的边框

十一、磨砂玻璃效果

十二、FLIP动画

一、弧形边框选项卡

 <style>
      .tab {
        width: 150px;
        height: 40px;
        color: #fff;
        text-align: center;
        line-height: 40px;
        margin: 0 auto;
        background: #ed6a5e;
        border-radius: 10px 10px 0 0;
        position: relative;
        transform: perspective(30px) rotateX(13deg);
        transform-origin: center bottom; /* 以tab最下方的线为中心进行旋转 */
      }
      .tab::before,
      .tab::after {
        content: "";
        position: absolute;
        width: 10px;
        height: 10px;
        bottom: 0;
        background: #000;
      }
      .tab::before {
        left: -10px;
        background: radial-gradient(
          circle at 0 0,
          transparent 10px,
          #ed6a5e 10px
        ); /* 左边以左上角为圆点进行径向渐变 */
      }
      .tab::after {
        right: -10px;
        background: radial-gradient(
          circle at 100% 0,
          transparent 10px,
          #ed6a5e 10px
        ); /* 右边以右上角为圆点进行径向渐变 */
      }
    </style>

二、零宽字符

    <script>
      // 判断字符串中是否包含零宽字符
      function containsZeroWidthCharacter(str) {
        // 正则表达式匹配零宽字符
        const zeroWidthRegex = /[\u200B\u200C\u200D\uFEFF]/g;
        return zeroWidthRegex.test(str);
      }
      console.log(containsZeroWidthCharacter("Hello\u200B World")); // true
      console.log(containsZeroWidthCharacter("Hello World")); // false
    </script>

常见的零宽字符包括:

1、零宽空格【Unicode:U+200B】
这是一个没有宽度的空格,可以在两个字符之间插入,但不会在视觉上产生间隔。
2、零宽非连接符【Unicode:U+200C】
用于阻止字符连接。在阿拉伯语或印地语中,用来控制哪些字符应该连接,哪些不应连接。
3、零宽连接符【Unicode:U+200D】
用于强制某些字符连接在一起,这通常在一些复合字符或表情符号中起作用。与2相反
4、零宽非换行空格【Unicode:U+FEFF】
用于文件中的字节顺序标记,但也可以用作零宽空格的一种形式。作用是防止换行。

尽管零宽字符对用户不可见,但它们会占用存储空间,通常用于文本隐写防止链接自动化处理格式化和排版。比如文本处理时加上零宽字符,可以防止文本被盗窃,解码后是自己的名称。

三、目录滚动时自动高亮

    <style>
      body {
        display: flex;
        margin: 0;
      }
      /* 左侧目录 */
      .sidebar {
        width: 250px;
        background-color: #333;
        color: white;
        padding: 20px;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        overflow-y: auto;
      }
      .sidebar a {
        color: white;
        text-decoration: none;
        display: block;
        padding: 10px;
        margin: 5px 0;
      }
      .sidebar a:hover {
        background-color: #575757;
      }
      .highlight {
        background-color: #ffcc00; /* 高亮颜色 */
      }
      /* 右侧内容 */
      .content {
        margin-left: 280px; /* 留出左侧目录栏空间 */
        padding: 20px;
        width: calc(100% - 260px);
      }
      h1 {
        color: #333;
      }
      .section {
        margin-bottom: 30px;
        height: 750px;
        border: 1px solid yellowgreen;
      }
      .section h2 {
        color: #444;
      }
    </style>
  <body>
    <div class="sidebar toc">
      <h2>目录</h2>
      <a href="#section1">部分 1</a>
      <a href="#section2">部分 2</a>
      <a href="#section3">部分 3</a>
      <a href="#section4">部分 4</a>
    </div>
    <div class="content">
      <div class="section" id="section1">
        <h2>部分 1</h2>
        <p>第一部分内容。</p>
      </div>
      <div class="section" id="section2">
        <h2>部分 2</h2>
        <p>第二部分内容。</p>
      </div>
      <div class="section" id="section3">
        <h2>部分 3</h2>
        <p>第三部分内容。</p>
      </div>
      <div class="section" id="section4">
        <h2>部分 4</h2>
        <p>第四部分内容。</p>
      </div>
    </div>
    <script>
      function highlight(id) {
        document
          .querySelectorAll("a.highlight")
          .forEach((a) => a.classList.remove("highlight"));
        if (id instanceof HTMLElement) {
          id.classList.add("highlight");
          return;
        }
        if (id.startsWith("#")) {
          id = id.substring(1);
        }
        document.querySelector(`a[href="#${id}"]`).classList.add("highlight");
      }
      const links = document.querySelectorAll('.toc a[href^="#"]');
      const titles = [];
      for (const link of links) {
        link.addEventListener("click", () => {
          highlight(link.getAttribute("href").substring(1));
        });
        const url = new URL(link.href);
        const dom = document.querySelector(url.hash);
        if (dom) {
          titles.push(dom);
        }
      }
      function debounce(fn, delay = 100) {
        let timer = null;
        return function (...args) {
          clearTimeout(timer);
          timer = setTimeout(() => {
            fn.apply(this, args);
          }, delay);
        };
      }
      const scrollHandler = debounce(() => {
        const rects = titles.map((title) => title.getBoundingClientRect());
        const range = 300;
        for (let i = 0; i < titles.length; i++) {
          const title = titles[i];
          const rect = rects[i];
          // 标题区域在指定范围内就高亮
          if (rect.top >= 0 && rect.top <= range) {
            highlight(title.id);
            break;
          }
          // 当前内容标题在展示视口之上,并且下一个标题在展示视口之下,此时高亮此标题
          if (
            rect.top < 0 &&
            rects[i + 1] &&
            rects[i + 1].top > document.documentElement.clientHeight
          ) {
            highlight(title.id);
            break;
          }
        }
      }, 100);
      window.addEventListener("scroll", scrollHandler);
    </script>
  </body>

四、高亮关键字

<style>
      .highlight {
        color: red;
        font-weight: bold;
      }
</style>
  <body>
    <div>
      <input type="text" class="txtKeyword" />
      <ul></ul>
    </div>
    <script>
      const ul = document.querySelector("ul");
      const txtKeyword = document.querySelector(".txtKeyword");
      function setHTML(lists) {
        ul.innerHTML = lists
          .map((l) => {
            let cname = l.cname;
            // 如果输入框有内容,则进行高亮匹配
            if (txtKeyword.value) {
              const reg = new RegExp(txtKeyword.value, "ig");
              cname = cname.replace(reg, function (key) {
                return `<span class="highlight">${key}</span>`;
              });
            }
            return `<li><span>${cname}</span></li>`;
          })
          .join(""); // 使用 join 合并生成的 HTML 字符串
      }
      const NameLists = [
        { cname: "前端" },
        { cname: "后端" },
        { cname: "测试员" },
        { cname: "运维师" },
      ];
      // 筛选包含关键字的元素
      function filterList() {
        const keyword = txtKeyword.value.trim();
        if (keyword) {
          const filtered = NameLists.filter((item) =>
            item.cname.match(new RegExp(keyword, "i"))
          );
          setHTML(filtered);
        } else {
          setHTML(NameLists);
        }
      }
      setHTML(NameLists);
      // 给输入框添加监听事件,以便动态更新
      txtKeyword.addEventListener("input", filterList);
    </script>
  </body>

五、文字描边

第一种text-shadow:给8个方向即可,但是连接处有瑕疵(见红色边缘部分)

@mixin text-stroke($color: #fff, $width: 1px) {
  text-shadow: 0 -#{$width} #{$color}, #{$width} 0 #{$color},
    0 #{$width} #{$color}, -#{$width} 0 #{$color}, #{$width} #{$width} #{$color},
    -#{$width} -#{$width} #{$color}, #{$width} -#{$width} #{$color},
    -#{$width} #{$width} #{$color};
}
p {
  font-size: 50px;
  font-weight: bold;
  @include text-stroke(red, 2px);
  // color: transparent; 不支持文字透明
}
p {
        font-size: 50px;
        font-weight: bold;
        text-shadow: 0 -2px gold, 2px 0 gold, 0 2px gold, -2px 0 gold,
          /* 上、右、下、左 */ 2px 2px gold, -2px -2px gold, 2px -2px gold,
          -2px 2px gold; /* 四个对角线 */
}

第二种-webkit-text-stroke不仅边缘平滑,并且支持透明

p {
  font-size: 50px;
  font-weight: bold;
  -webkit-text-stroke: 2px red;
  color: transparent; //支持文字透明
  position: relative;
}
// -webkit-text-stroke是居中描边,原来字体变小了,可以在"画一层"盖上去
p::after {
  content: attr(data-text);
  position: absolute;
  left: 0;
  top: 0;
  -webkit-text-stroke: 0;
}

六、按钮边框的旋转动画

原理:在按钮层级下加一个矩形,围绕按钮中心进行360度旋转,多余矩形隐藏

    <style>
      button {
        width: 100px;
        height: 50px;
        color: white;
        outline: none;
        z-index: 1;
        border-radius: 10px;
        cursor: pointer;
        background: black;
        /* outline: 4px solid gold; */
        position: relative;
        overflow: hidden;
      }
      button::before {
        content: "";
        position: absolute;
        width: 200%;
        height: 200%;
        background: blue;
        z-index: -2;
        left: 50%;
        top: 50%;
        transform-origin: left top;/* 圆点在左上角 */
        animation: rotation 2s linear infinite;
      }
      button::after {
        content: "";
        position: absolute;
        --g: 4px;
        width: calc(100% - var(--g) * 2);
        height: calc(100% - var(--g) * 2);
        background: black;
        left: var(--g);
        top: var(--g);
        border-radius: inherit;
        z-index: -1;
      }
      @keyframes rotation {
        to {
          transform: rotate(360deg);
        }
      }
    </style>

七、视频文字特效

  <style>
    .txt{
      position:absolute;
      inset: 0;
      background: #fff;
      display: flex;
      justify-content: center;
      align-items: center;
      mix-blend-mode: screen; /* 增强亮度,使图像或元素显得更加明亮 */
    }
  </style>
<body>
  <div class="container">
    <video src="./fire.mp4" autoplay muted></video>
    <div class="txt">大前端</div>
  </div>
</body>

八、立体文字特效+让文字立起来

放大看是叠加出来的,真正要做得建模

    <style>
      body {
        background-color: brown;
        color: #fff;
        padding: 30px;
      }
      .text1 {
        font-size: 5em;
        text-shadow: -1px 1px #bbb, -2px 2px #bbb, -3px 3px #bbb, -4px 4px #bbb,
          -5px 5px #bbb, -10px 10px 3px #0008;
      }
      .text2 {
        font-weight: 700;
        position: relative;
      }
      .text2::after {
        content: "DARKNESS";
        position: absolute;
        left: 0;
        top: 0;
        color: #000;
        transform: translate(-25px, 2px) scale(0.9) skew(50deg);
        z-index:-1;
        filter: blur(2px);
        mask:linear-gradient(transparent,#000)
      }
    </style>
  <body>
    <h1 class="text1">立体文字</h1>
    <h1 class="text2">DARKNESS</h1>
  </body>

九、文字连续光影特效

span {
  color: #faebd7;
  animation: colorChange 1s infinite alternate;
}
@keyframes colorChange {
  to {
    color: #ff0266;
  }
}
@for $i from 1 through 7 {
  span:nth-child(#{$i}) {
    animation-delay: ($i - 1) * 0.1s;
  }
}

十、重复渐变的边框

    <style>
      .card {
        width: 217px;
        margin: 0 auto;
        color: #333;
        line-height: 1.8;
        border-radius: 10px;
        background: repeating-linear-gradient(
            -45deg,
            #e8544d 0 10px,
            #fff 10px 20px,
            #75adf8 20px 30px,
            #fff 30px 40px
          ) -20px -20px/200% 200%;
        padding: 5px;
        transition: 0.5s;
      }
      .card:hover {
        background-position: 0 0;
      }
      .container {
        background: #fff;
        border-radius: inherit;
      }
    </style>
  <body>
    <div class="card">
      <div class="container">
        重复渐变的边框原理:<br/>
        设置背景为重复的线性渐变。渐变角度为-45度,包含四个颜色区块。
      </div>
    </div>
  </body>

十一、磨砂玻璃效果

    <style>
      .wrap {
        text-align: center;
        color: white;
      }
      .modal {
        background: rgba(255, 255, 255, 0.4); /* 半透明背景 */
        backdrop-filter: blur(10px); /* 模糊背景 */
        border-radius: 15px; /* 圆角 */
        padding: 40px;
        width: 300px;
        box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); /* 增加阴影 */
        font-size: 24px;
      }
    </style>
  <body>
    <div class="wrap">
      <div class="modal">磨砂玻璃效果</div>
    </div>
  </body>

十二、各种FLIP动画

参考:https://zhuanlan.zhihu.com/p/712766286

    <style>
      .box {
        width: 60px;
        height: 60px;
        background-color: skyblue;
        color: white;
        font-size: 30px;
        margin: 10px;
      }
    </style>
  <body>
    <div class="container" style="display: flex">
      <div class="box" key="1">1</div>
      <div class="box" key="2">2</div>
      <div class="box" key="3">3</div>
      <div class="box" key="4">4</div>
      <div class="box" key="5">5</div>
    </div>
    <button onclick="shuffle()">打乱</button>
    <script>
      function shuffle() {
        const container = document.querySelector(".container");
        const boxes = Array.from(container.children);
        // First: 记录每个盒子的起始位置
        const startPositions = boxes.reduce(
          (result, box) => ({
            ...result,
            [box.getAttribute("key")]: box.getBoundingClientRect(),
          }),
          {}
        );
        // 随机打乱盒子顺序,然后把打乱好的盒子放回 DOM
        boxes.sort(() => Math.random() - 0.5);
        boxes.forEach((box) => container.appendChild(box));
        // Last: 记录每个盒子的最终位置
        const endPositions = boxes.reduce(
          (result, box) => ({
            ...result,
            [box.getAttribute("key")]: box.getBoundingClientRect(),
          }),
          {}
        );
        // Invert: 计算 “反向” 偏移量
        boxes.forEach((box) => {
          const key = box.getAttribute("key");
          const start = startPositions[key];
          const end = endPositions[key];
          // 注意,此时 DOM 已经处于最终位置,所以它的 translate 是 “反向” 的
          // 所以要用 first 来减去 last
          const deltaX = start.left - end.left;
          const deltaY = start.top - end.top;
          // 如果元素 “原地不动”,那么跳过后续流程
          if (deltaX === 0 && deltaY === 0) {
            return;
          }
          // 让元素通过 transform 偏移回到起点
          box.style.transition = null; // 暂时屏蔽掉过渡,实际生产此处需完善
          box.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
          // Play: 在重绘之前,撤掉 transform 偏移,播放 “归位” 过渡动画
          requestAnimationFrame(() => {
            box.style.transition = `transform 2s`;
            box.style.transform = "";
          });
          // FLIP 动画完成后,清理残余样式
          box.addEventListener(
            "transitionend",
            () => {
              box.style.transition = null;
              box.style.transform = null;
            },
            { once: true }
          );
        });
      }
    </script>
  </body>

其中的“Invert” 和 “Play” 步骤可以使用 Web Animation API 进行简化

    <style>
      .box {
        width: 60px;
        height: 60px;
        color: white;
        font-size: 30px;
        margin: 10px;
        box-sizing: border-box;
        background-color: skyblue;
        border: 2px black solid;
        transition: width 500ms, height 500ms;
      }
      .scale {
        position: absolute;
        top: 90px;
        left: 10px;
        width: 120px;
        height: 120px;
        z-index: 10;
      }
    </style>
  <body>
    <div class="container" style="display: flex">
      <div class="box" key="1">1</div>
      <div class="box" key="2">2</div>
      <div class="box" key="3">3</div>
      <div class="box" key="4">4</div>
      <div class="box" key="5">5</div>
    </div>
    <script>
      const container = document.querySelector('.container')
      const boxes = Array.from(container.children)
      boxes.forEach(box => {
        box.addEventListener('click', () => {
          // First: 记录每个盒子的起始位置
          const startPositions = boxes.reduce(
            (result, box) => ({
              ...result,
              [box.getAttribute('key')]: box.getBoundingClientRect(),
            }),
            {}
          )
          box.classList.toggle('scale')
          // Last: 记录每个盒子的最终位置
          const endPositions = boxes.reduce(
            (result, box) => ({
              ...result,
              [box.getAttribute('key')]: box.getBoundingClientRect(),
            }),
            {}
          )
          // Invert: 计算 “反向” 偏移量
          boxes.forEach(box => {
            const key = box.getAttribute('key')
            const start = startPositions[key]
            const end = endPositions[key]
            // 注意,此时 DOM 已经处于最终位置,所以它的 transform 是 “反向” 的
            // 所以要用 first 来减去 last
            const deltaX = start.left - end.left
            const deltaY = start.top - end.top
            // 如果元素 “原地不动”,那么跳过后续流程
            if (deltaX === 0 && deltaY === 0) {
              return
            }
            // 将盒子通过 transform 移至初始位置
            box.style.transition = ''
            box.style.transform = `translate(${deltaX}px, ${deltaY}px)`
            // Play: 播放动画应用变换
            requestAnimationFrame(() => {
              box.style.transition = `all 500ms`
              box.style.transform = ''
            })
            // FLIP 动画完成后,清理残余样式
            box.addEventListener(
              'transitionend',
              () => {
                box.style.transition = null
                box.style.transform = null
              },
              { once: true }
            )
          })
        })
      })
    </script>
  </body>

Vue 内置组件 <TransitionGroup> 已经实现了 FLIP 动画

<template>
  <TransitionGroup style="display: flex" name="flip" tag="div">
    <div class="box" v-for="item of list" :key="item">
      {{ item }}
    </div>
  </TransitionGroup>
  <button @click="shuffle">打乱</button>
</template>
<script setup>
import { reactive } from 'vue'
const list = reactive([1, 2, 3, 4, 5])
const shuffle = () => void list.sort(() => Math.random() - 0.5)
</script>
<style scoped>
.box {
  width: 60px;
  height: 60px;
  background-color: skyblue;
  color: white;
  font-size: 30px;
  margin: 10px;
}
.flip-move {
  transition: all 2s;
}
</style>

react-flip-toolkit 工具是一个用于实现组件 FLIP 动画的 React 库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值