Vue3 实现最近很火的酷炫功能:卡片悬浮发光

2024年 5月 22日 109.5k 0

有趣的动画效果

前几天在网上看到了一个很有趣的动画效果,如下,光会跟随鼠标在卡片上进行移动,并且卡片会有视差的效果。

那么在 Vue3 中应该如何去实现这个效果呢?

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-1图片

基本实现思路

其实实现思路很简单,无非就是分几步:

  • 首先,卡片是相对定位,光是绝对定位
  • 监听卡片的鼠标移入事件mouseenter,当鼠标进入时显示光
  • 监听卡片的鼠标移动事件mouseover,鼠标移动时修改光的left、top,让光跟随鼠标移动
  • 监听卡片的鼠标移出事件mouseleave,鼠标移出时,隐藏光

我们先在 Index.vue 中准备一个卡片页面,光的CSS效果可以使用filter: blur() 来实现。

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-2图片

可以看到现在的效果是这样:

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-3图片

实现光源跟随鼠标

在实现之前我们需要注意几点:

1、鼠标移入时需要设置卡片 overflow: hidden,否则光会溢出,而鼠标移出时记得还原。

2、获取鼠标坐标时需要用clientX/Y而不是pageX/Y,因为前者会把页面滚动距离也算进去,比较严谨。

刚刚说到实现思路时我们说到了mouseenter、mousemove、mouseleave,其实mouseenter、mouseleave 这二者的逻辑比较简单,重点是 mouseover 这个监听函数。

而在 mouseover 这个函数中,最重要的逻辑就是:光怎么跟随鼠标移动呢?

或者也可以这么说:怎么计算光相对于卡片盒子的 left 和 top。

对此我专门画了一张图,相信大家一看就懂怎么算了。

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-4图片

  • left = clientX - x - width/2
  • height = clientY - y - height/2

知道了怎么计算,那么逻辑的实现也很明了了~封装一个use-light-card.ts:

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-5图片

接着在页面中去使用:

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-6图片

这样就能实现基本的效果啦~

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-7图片

卡片视差效果

卡片的视差效果需要用到样式中 transform 样式,主要是配置四个东西:

  • perspective:定义元素在 3D 变换时的透视效果
  • rotateX:X 轴旋转角度
  • rotateY:Y 轴旋转角度
  • scale3d:X/Y/Z 轴上的缩放比例

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-8图片

现在就有了卡片视差的效果啦~

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-9图片

给所有卡片添加光源

上面只是给一个卡片增加光源,接下来可以给每一个卡片都增加光源啦!!!

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-10图片

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-11图片

让光源变成可配置

上面的代码,总感觉这个 hooks 耦合度太高不太通用,所以我们可以让光源变成可配置化,这样每个卡片就可以展示不同大小、颜色的光源了~像下面一样。

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-12图片

既然是配置化,那我们希望是这么去使用 hooks 的,我们并不需要自己在页面中去写光源的dom节点,也不需要自己去写光源的样式,而是通过配置传入 hooks 中。

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-13

所以 hooks 内部要自己通过操作 DOM 的方式,去添加、删除光源,可以使用createElement、appendChild、removeChild 去做这些事~。

Vue3 实现最近很火的酷炫功能:卡片悬浮发光-14图片

完整源码




  
    
    
    
    
    
    
  




// use-light-card.ts

import { onMounted, onUnmounted, ref } from 'vue';

interface IOptions {
  light?: {
    width?: number; // 宽
    height?: number; // 高
    color?: string; // 颜色
    blur?: number; // filter: blur()
  };
}

export const useLightCard = (option: IOptions = {}) => {
  // 获取卡片的dom节点
  const cardRef = ref(null);
  let cardOverflow = '';
  // 光的dom节点
  const lightRef = ref(document.createElement('div'));
  // 设置光源的样式

  const setLightStyle = () => {
    const { width = 60, height = 60, color = '#ff4132', blur = 40 } = option.light ?? {};
    const lightDom = lightRef.value;
    lightDom.style.position = 'absolute';
    lightDom.style.width = `${width}px`;
    lightDom.style.height = `${height}px`;
    lightDom.style.background = color;
    lightDom.style.filter = `blur(${blur}px)`;
  };

  // 设置卡片的 overflow 为 hidden
  const setCardOverflowHidden = () => {
    const cardDom = cardRef.value;
    if (cardDom) {
      cardOverflow = cardDom.style.overflow;
      cardDom.style.overflow = 'hidden';
    }
  };
  // 还原卡片的 overflow
  const restoreCardOverflow = () => {
    const cardDom = cardRef.value;
    if (cardDom) {
      cardDom.style.overflow = cardOverflow;
    }
  };

  // 往卡片添加光源
  const addLight = () => {
    const cardDom = cardRef.value;
    if (cardDom) {
      cardDom.appendChild(lightRef.value);
    }
  };
  // 删除光源
  const removeLight = () => {
    const cardDom = cardRef.value;
    if (cardDom) {
      cardDom.removeChild(lightRef.value);
    }
  };

  // 监听卡片的鼠标移入
  const onMouseEnter = () => {
    // 添加光源
    addLight();
    setCardOverflowHidden();
  };

  // use-light-card.ts

  // 监听卡片的鼠标移动
  const onMouseMove = (e: MouseEvent) => {
    // 获取鼠标的坐标
    const { clientX, clientY } = e;
    // 让光跟随鼠标
    const cardDom = cardRef.value;
    const lightDom = lightRef.value;
    if (cardDom) {
      // 获取卡片相对于窗口的x和y坐标
      const { x, y } = cardDom.getBoundingClientRect();
      // 获取光的宽高
      const { width, height } = lightDom.getBoundingClientRect();
      lightDom.style.left = `${clientX - x - width / 2}px`;
      lightDom.style.top = `${clientY - y - height / 2}px`;

      //   设置动画效果
      const maxXRotation = 10; // X 轴旋转角度
      const maxYRotation = 10; // Y 轴旋转角度

      const rangeX = 200 / 2; // X 轴旋转的范围
      const rangeY = 200 / 2; // Y 轴旋转的范围

      const rotateX = ((clientX - x - rangeY) / rangeY) * maxXRotation; // 根据鼠标在 Y 轴上的位置计算绕 X 轴的旋转角度
      const rotateY = -1 * ((clientY - y - rangeX) / rangeX) * maxYRotation; // 根据鼠标在 X 轴上的位置计算绕 Y 轴的旋转角度

      cardDom.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; //设置 3D 透视
    }
  };
  // 监听卡片鼠标移出
  const onMouseLeave = () => {
    // 鼠标离开移出光源
    removeLight();
    restoreCardOverflow();
  };

  onMounted(() => {
    // 设置光源样式
    setLightStyle();
    // 绑定事件
    cardRef.value?.addEventListener('mouseenter', onMouseEnter);
    cardRef.value?.addEventListener('mousemove', onMouseMove);
    cardRef.value?.addEventListener('mouseleave', onMouseLeave);
  });

  onUnmounted(() => {
    // 解绑事件
    cardRef.value?.removeEventListener('mouseenter', onMouseEnter);
    cardRef.value?.removeEventListener('mousemove', onMouseMove);
    cardRef.value?.removeEventListener('mouseleave', onMouseLeave);
  });

  return {
    cardRef,
  };
};

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论