likes
comments
collection
share

✨✨做一个伪3D效果的卡片列表来复习一下CSS动画吧~

作者站长头像
站长
· 阅读数 19

前言

本来是准备接着前面做的那个 使用SVG实现动态分布的圆环发散路径动画 的效果,希望通过 纯 Div + CSS 的方式来实现。但是目前看起来进度比较缓慢,虽然做出来了大致样式,但是动画还没加上,所以得后面再继续弄。

今天主要是通过写一个 “伪3D” 的卡片组效果,顺道也复习一下基础的 CSS 动画。

因为作者平时都是写的后台管理的项目,所以接触 CSS 动画之类的东西也比较少,实现方式和效果看起来可能没有那么好,希望大家多多包涵。

开始

本身这里最初的设想是希望 像打牌那样的初始效果,然后 通过点击卡片抽出单张选中卡片预览,再加上一些简单的过渡动画。不过后来又觉得 直接做成平铺列表然后选中卡片移动到最右侧 的效果也行,所以干脆都加上算了。

当然为了保证正确的层级关系,还是使用了 定位配合 zIndex 来确定每个卡片的 层级顺序和位置

卡片的排列模式也是通过一个变量来确定的,然后增加了一个切换按钮,后面如果有需要的话改成 props 配置也行。

话不多说,直接开干吧。

模拟数据

当然,在开始之前肯定要 模拟一个卡片数组 cards 与一个排列模式变量 clutter

data() {
  return {
    activeIndex: -1,
    clutter: true, // 杂乱
    cards: []
  };
},
created() {
  this.initData();
},
methods: {
  initData() {
    const arr = new Array(12).fill(1);
    this.cards = arr.map((_, index) => {
      return this.computedStyle(index, 12);
    });
  },
  resetData() {
    this.clutter = !this.clutter;
    this.initData();
  },
  computedStyle(index, length) {
    const clutter = this.clutter;
    const defaultStyles = {
        "--max-index": length + 1,
        "--bg-color": randomRgbColor(),
        "--card-index": index
    };

    return defaultStyles;
  }
}

这里的 initDataresetData 肯定就不需要介绍啦,就是重置和切换排列模式,当然因为改变排列模式后样式也有改动,所以在resetData 中也重新调用了initData来重新生成卡片数组。

至于computedStyle则是 计算每个卡片的样式并返回样式结果的,不管什么排列模式下 这几个 CSS 变量都是必须且固定的,所以就先放上来了。

data 中的activeIndex 则是代表当前激活的卡片下标的。

键盘事件

这里的初衷是为了方便 快速切换激活卡片、在卡片太多时也可以减少误触

这里唯一注意的一点就是,在执行键盘监听事件的时候,需要判断一下当前的 focus 元素,如果是在一些可聚集的元素中则不能触发翻页

mounted() {
  let addIndex = () => {
    if (this.activeIndex < this.cards.length - 1) {
      this.activeIndex++;
    } else {
      this.activeIndex = 0;
    }
  };
  let lessIndex = () => {
    if (this.activeIndex > 0) {
      this.activeIndex--;
    } else {
      this.activeIndex = this.cards.length - 1;
    }
  };
  let keyboardDeal = (e) => {
    if (document.activeElement !== document.body) return;
    // 方向键--上
    if (e.keyCode === 38) {
      addIndex();
    }
    // 方向键--下
    if (e.keyCode === 40) {
      lessIndex();
    }
    // 方向键--左
    if (e.keyCode === 37) {
      lessIndex();
    }
    // 方向键--右
    if (e.keyCode === 39) {
      addIndex();
    }
  };

  window.addEventListener("keyup", keyboardDeal);
  this.$on("hook:beforeDestroy", () => {
    window.removeEventListener("keyup", keyboardDeal);
  });
},

另外,这里通过 this.$on("hook:beforeDestroy") 来注册组件 在销毁之前需要执行的回调函数功能与选项式 API 中的 beforeDestroy 配置项功能一致

在组件即将销毁时,会清除掉组件中的键盘监听事件。

通过document.activeElement 是否等于 body 元素,来确定当前焦点位置。一般只有在该属性等于body的时候,才代表当前页面中没有其他聚焦元素,可以正常执行我们定义的按键翻页。

模板

至于 html 部分,倒是没有太多细节的内容,仅仅只是 Vue 的遍历语法和动态样式两个知识点,相信大部分同学都很清楚。

<template>
  <div class="AnimationCards">
    <h1>AnimationCards Page</h1>
    <p>
      <el-button @click="resetData">乱序</el-button>
    </p>

    <div class="demo-content">
      <div class="animation-cards-box">
        <div
          v-for="(styles, index) in cards"
          :class="[
            'animation-card',
            { 'is-active': activeIndex === index, 'is-clutter': clutter, 'is-list': !clutter }
          ]"
          :key="index"
          :style="styles"
          @click="activeIndex = activeIndex === index ? -1 : index"
        >
          <span>Card {{ index }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

这里动态绑定的 is-clutter 和 is-list 其实也可以放到父级 animation-cards-box 中

至于每个 card 元素动态绑定的 style,里面有定义好的 CSS 变量,提供给每个卡片的动画和默认样式使用。这个用法我在之前的文章中也提到过,标签的内联样式也可以直接声明 CSS 变量

样式与动画

因为采用了两种排列方式,每种排列方式的动画都不一样,所以样式和动画都有不同。但是因为共同都是采用的定位来确定层级的,所以也有一样的地方。

下面这部分是一样的样式部分:

.animation-cards-box {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 20px;
  position: relative;
  .animation-card {
    width: 200px;
    background-color: var(--bg-color);
    border-radius: 8px;
    cursor: pointer;
    position: absolute;
    top: 20px;
    bottom: 20px;
    box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.8);
    transition: all ease-in-out 0.4s;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 32px;
    font-weight: bold;
    color: #ffffff;
    z-index: var(--card-index);
  }
}

通过上面模板部分绑定的 CSS 变量来确定具体样式。

列表状态

列表分布的状态下,所有卡片 依次向右排布,后面的卡片覆盖前面的卡片;在卡片被激活后,会移动到最右侧最顶层显示,大致效果如下:

✨✨做一个伪3D效果的卡片列表来复习一下CSS动画吧~

1. 所以,首先是正常情况下的分布样式

.animation-card.is-list {
  &:hover {
    & ~ .animation-card {
      transform: translateX(24px);
    }
  }
  &.is-active {
    z-index: var(--max-index) !important;
    transform: translateX(calc(var(--max-index) * 20px - var(--card-index) * 20px - 40px));
  }
}

在 hover 时,所有 后面的兄弟节点会向右移动 20px;激活后(is-active),被激活的卡片会通过 zIndex 置顶,并通过 transform 向右平移到 最右侧

2. 然后,是 js 中的 computedStyle 样式计算,按顺序每个卡片向右移动 16 px(后面可以根据需求修改)

computedStyle(index, length) {
    const clutter = this.clutter;
    const defaultStyles = {
        "--max-index": length + 1,
        "--bg-color": randomRgbColor(),
        "--card-index": index
    };

    if (!clutter) defaultStyles["left"] = `${16 * ++index}px`;

    return defaultStyles;
}

3. 最后,是列表横向排布时的动画帧

@keyframes eject {
  50% {
    transform: translateX(calc(-100% - 20px)) rotate(-20deg);
  }
}
@keyframes reject {
  50% {
    transform: translateX(calc(100% + 20px)) rotate(10deg);
  }
}

这里区分了 非激活 -> 被激活 的 eject从被激活 -> 非激活状态的 reject 两个动画:

  • eject :动画中间状态是 整体 向左偏移 整个卡片宽度加上 20px 的距离,并向左稍微旋转 20deg
  • reject :动画中间状态是 整体 向右偏移 整个卡片宽度加上 20px 的距离,并向右稍微旋转 10deg

另外eject的动画时间比reject 多一倍,也是为了把注意力集中在被激活卡片。

本身我是希望最后才把 zIndex 设置成最大值的,但是因为用了CSS变量的关系,动画帧定义不知道咋写了。如果有大佬知道也请告诉我一下,非常感谢~~

扇形乱序分布

这也是我里边说“乱序”状态吧,因为最初的一版卡片分布是按下标依次一左一右排列的,所以顺序有一点问题;不过后面也加了一个完整扇形的效果。

基础样式与列表状态一样,有区别的只是 激活/非激活的样式计算和动画定义部分

这个效果大概像这样:

✨✨做一个伪3D效果的卡片列表来复习一下CSS动画吧~

1. 首先,一样是两个状态的样式定义

.animation-card.is-clutter {
  transform: translateX(0%) rotate(var(--rotate-deg));
  transform-origin: bottom center;
  &.is-active {
    animation: rotation ease-in-out 0.8s;
    transform: translateX(calc(120%)) rotate(0deg);
    z-index: var(--max-index) !important;
  }
}

因为是 类似扇形的效果,需要保证旋转轴的位置在底部,所以需要设置 transform-origin

  • 正常状态 下,卡片只是稍微旋转
  • 被激活状态 下,卡片会平移到最后侧并取消旋转角度,同时置顶

2. 然后,是样式计算部分

computedStyle(index, length) {
  const clutter = this.clutter;
  const defaultStyles = {
    "--max-index": length + 1,
    "--bg-color": randomRgbColor(),
    "--card-index": index
  };

  if (clutter) {
    let rotate = 0;
    if (index % 2 === 1) {
      rotate = length - index;
    } else {
      rotate = index - length;
    }
    defaultStyles["--rotate-deg"] = rotate + "deg";
  }

  return defaultStyles;
}

根据下标的奇偶性,分别向左、向右旋转特定角度。

3. 最后,就是动画定义

这里为了方便,只定义了 从 非激活 -> 激活状态 的动画,取消激活时则是直接通过 transition 定义的一个过渡效果。

@keyframes rotation {
  0% {
    transform: translateX(0%) rotate(var(--rotate-deg));
  }
  60% {
    transform: translateX(calc(130%)) rotate(2deg);
  }
  70% {
    transform: translateX(calc(110%)) rotate(-2deg);
  }
  80% {
    transform: translateX(calc(125%)) rotate(1deg);
  }
  90% {
    transform: translateX(calc(115%)) rotate(-1deg);
  }
  100% {
    transform: translateX(calc(120%)) rotate(0deg);
  }
}

这里动画 初期依旧保持正常状态,到 60% 时到达目标区域,后面的过程就是一个轻微晃动的效果,在100% 时停留在目标位置。

扇形正序

早上突然发现,正序其实修改不大,只需要调整 computedStyle 方法即可。

computedStyle(index, length) {
  const clutter = this.clutter;
  const defaultStyles = {
    "--max-index": length + 1,
    "--bg-color": randomRgbColor(),
    "--card-index": index
  };

  const tangle = 48;
  const unitArc = tangle / length;

  if (clutter) {
    let rotate = unitArc * index - 48 / 2;
    defaultStyles["--rotate-deg"] = rotate + "deg";

  return defaultStyles;
}

这里的 tangle 是两边的最大旋转角度,可以根据需要自定义。unitArc 则是单位旋转角度,然后 初始角度 则是在左边 二分之一 tangle 的位置。

此时,显示效果就是这样了:

✨✨做一个伪3D效果的卡片列表来复习一下CSS动画吧~

最后

当然本文个人感觉干货不是很多,只是在平时突然想到的一点儿小点子,但是动画效果其实不是很完美。只希望能给大家带来一点小小的灵感吧~~~

转载自:https://juejin.cn/post/7158848986659422244
评论
请登录