借助CSS实现一个花里胡哨的点赞粒子动效

欢迎关注我的公众号:前端侦探

好久不见!

大部分点击效果都平平无奇,如果你碰到一个这样的点击效果,会不会忍不住多点几次呢?

看似有些复杂,其实分解开来,其实就两个动画

  1. 粒子的抛物线动画
  2. 数字的缩放动画

下面来一步步实现它,花两分钟一起看看吧

一、CSS 抛物线运动

先从单个粒子看,其实就是一个抛物线运动,我们先从平抛运动开始。

众所周知,抛物线运动是一个水平方向上匀速、垂直方向上匀加速的合成运动

这个其实用 CSS 动画也很好实现,水平和垂直两个方向的位移动画分别用不同的动画缓存函数

有兴趣的可以参考张鑫旭的这篇文章:这回试试使用CSS实现抛物线运动效果

这里简单介绍一下

实现这样的效果需要一个嵌套结构

ini 复制代码
<div class="ball-x">
  <div class="ball-y"></div>
</div>

然后里外设置不同的动画缓冲函数

css 复制代码
.ball-x { 
  animation-timing-function: linear; /*匀速直线运动*/
}
.ball-y { 
  animation-timing-function: cubic-bezier(.55, 0, .85, .36);  /*大致匀加速运动*/
}

运动分解效果如下:

当然,这里为了兼容性考虑,使用了两层标签,其实还可以使用单层标签实现,需要用到动画合成属性,有兴趣可以参考之前这篇文章: 了解一下全新的CSS动画合成属性animation-composition

回到我们的例子,假设单个粒子的HTML是这样的

css 复制代码
<div class="custom-tips" style="left: 50%; top: 50%;">
    <div class="custom-tips-dot" emoji="🎉"></div>
 </div> 

关键样式如下

css 复制代码
.custom-tips {
  position: absolute;
  width: 1em;
  height: 1em;
  margin-left: -.5em;
  margin-top: -.5em;
  left: 0;
  top: 0;
  transform: translate(var(--left, 50%), var(--top, 50%));
}
.custom-tips-dot {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
  animation: custom-x 1s linear forwards;
}
.custom-tips-dot::before {
  content: attr(emoji, '🎉');
  animation: custom-y 1s cubic-bezier(0.55, 0, 0.85, 0.36);
}
​
/* x方向 */
@keyframes custom-x {
  100% {
    transform: translateX(300%);
  }
}
/* y方向 */
@keyframes custom-y {
  100% {
    transform: translateY(300%);
  }
}

效果如下(绿色线框是水平运动)

这样就简单实现了一个平抛运动

二、CSS 斜抛运动

平抛运动可能没有那种礼花绽放的感觉,有点平淡,我们还需要有一定角度朝上做斜抛运动。

看着好像有点复杂?其实就是多了一个向上的初始速度,示意如下

这个在 JS中很好实现,给一个初始值就行,垂直方向上先往上减速,一直到 0,然后开始加速,这样整体就是一个斜抛运动了。

那么,CSS 如何处理呢?好像并没有这种概念。

其实呢,可以从缓冲函数入手,比如上面用到的缓冲函数是cubic-bezier(0.55, 0, 0.85, 0.36),是一种逐渐变快的运动

这时,我们可以改变第一个锚点,往下拉,直到第二个值出现负数,如下

这样会出现一个轻微的回弹效果,效果如下

是不是有点像斜抛运动了?

我们可以继续调整幅度,让回弹效果更强烈一些,比如

这样就更接近真实的斜抛运动了

关键 CSS 如下

css 复制代码
.custom-tips-dot::before {
    content: attr(emoji, '🎉');
    animation: custom-y 1s cubic-bezier(0.56, -1.35, 0.85, 0.36) forwards;
  }

是不是也比较容易?

三、借助JS批量生产

单个粒子可能比较死板,也不够随机。

要实现随机也比较容易,每次生成例子的水平位移不同就行了,然后多个粒子的动画有一定的延时就可以了。

为了方便JS透传过去,可以借助CSS变量来生成

这里用--x表示水平位移,--d表示延迟

css 复制代码
.custom-tips-dot {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
  outline: 1px solid yellowgreen;
  animation: custom-x 1s var(--d, 0s) linear forwards; 
}
.custom-tips-dot::before {
  content: attr(emoji, '🎉');
  animation: custom-y 1s var(--d, 0s) cubic-bezier(0.56, -1.35, 0.85, 0.36) forwards;
}
/* x方向 */
@keyframes custom-x {
  0% {
    opacity: 0;
    transform: translateX(0%)
  }
  10%, 90% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translateX(var(--x, 300%)); /*随机的水平位移*/
  }
}

然后用 JS动态生成这些 dom

javascript 复制代码
function createDots(emojis) {
  const temp = document.createDocumentFragment();
  const random_emojis = emojis.slice(0, Math.ceil(Math.random() * emojis.length)).sort(() => Math.random() - .5)
  random_emojis.forEach(emoji => {
    const dot = document.createElement('div');
    dot.className = 'custom-tips-dot';
    dot.setAttribute('emoji', emoji);
    dot.style.setProperty('--d', `${Math.random() * 0.2}s`);
    dot.style.setProperty('--x', `${(Math.random() - 0.5) * 1000}%`);
    temp.appendChild(dot);
    /*动画结束后自动移除dom*/
    dot.addEventListener('animationend', () => {
      console.log('dot.parentNode', dot.parentNode, dot.parentNode?.childElementCount)
      if (dot?.parentNode?.childElementCount <= 1) {
        dot.parentNode.remove();
      } else {
        dot.remove();
      }
    });
  })
  return temp;
}
​
document.addEventListener('click',(ev) => {
  const { clientX, clientY } = ev;
  console.log(clientX, clientY);
  document.body.style.setProperty('--left', `${clientX}px`);
  document.body.style.setProperty('--top', `${clientY}px`);
  const tips = document.createElement('div');
  tips.style.setProperty('--left', `${clientX}px`);
  tips.style.setProperty('--top', `${clientY}px`);
  tips.className = 'custom-tips';
  const dots = createDots(['🎉', '😘', '🎊', '🤡', '🥳', '🤪', '💗']);
  tips.appendChild(dots);
  document.body.appendChild(tips);
})

这样就能每次随机生成一定数量的emoji了,效果如下

四、+1 动画

数字变化动画是最简单的,就是一个缩放+透明度变化的动画。

假设HTML是这样的

ini 复制代码
<div class="custom-num" num="1"></div>

我们用伪元素来生成+1的字符,动画很简单,就两个关键帧,实现如下

css 复制代码
.custom-num {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  width: 2em;
  height: 2em;
  font-size: 2em;
  color: #fff;
  justify-content: center;
  align-items: center;
  margin-left: -1em;
  margin-top: -2em;
  font-weight: bold;
  text-shadow: 4px 4px 0 rgba(255,0,0);
  transform: translate(var(--left), var(--top));
}
.custom-num::before {
  content: '+' attr(num);
  opacity: 0;
  animation: count-shark 1s var(--d, 0s);
}
@keyframes count-shark {
  0%,100%{
    opacity: 0;
    transform: scale(.4);
  }
​
  30%,70%{
    opacity: 1;
    transform: scale(1);
  }
}

效果如下

然后我们借助JS来实现数字增加的逻辑,其实就是移除上一个,新增一个dom,新增的会自动执行动画,实现如下

ini 复制代码
function createNum() {
  const current = document.querySelector('.custom-num');
  let num = 1
  if (current) {
    num = parseInt(current.getAttribute('num')) + 1;
    current.remove();
  }
  const numDiv = document.createElement('div');
  numDiv.className = 'custom-num';
  if (num > 1) {
    // numDiv.style.setProperty('--d','-.3s' )
  }
  numDiv.setAttribute('num', num);
  numDiv.addEventListener('animationend', () => {
    numDiv.remove();
  });
  return numDiv;
}

最后两者结合起来,就实现了文章开头所示效果

你也可以访问在线demo真实体验:codepen.io/xboxyan/pen...

五、其实canvas可能更适合

一般来说,粒子动画可能canvas来实现更合适一点,性能也更好,不过可能对新手不太友好。这里推荐一个开箱即用的库:叫做 Canvas Confetti,专门用来做这种礼花绽放特效的,如果自己手搓不来,可以试试这个。

不过我们这里案例的粒子也不是很多,所以使用CSS也无所谓了。

关于动画的合成、分解、叠加小技巧,你学会了吗,赶紧在项目中用起来吧~最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤

相关推荐
l1t5 分钟前
使用流式函数解决v语言zstd程序解压缩失败问题
前端·压缩·v语言·zstd
小离a_a15 分钟前
el-tree方法的整理
前端·vue.js·elementui
90后的晨仔23 分钟前
Vercel部署完全指南:从踩坑到成功的实战经验分享
前端·vue.js
泯泷1 小时前
Tiptap 深度教程(三):核心扩展全面指南
前端·javascript·全栈
前端AK君1 小时前
rolldown-vite初体验
前端·前端工程化
zayyo1 小时前
大厂前端为什么都爱用pnpm + monorepo 做项目工程化架构?
前端
一枚前端小能手1 小时前
💎 Less/Sass写出优雅代码的4个高级技巧
css·less
桃桃乌龙_95271 小时前
受不了了,webpack3.x升级到webpack4.x
前端·webpack
青花雅月1 小时前
解决复用页面只是接口不同的问题的完整指南
前端
FogLetter1 小时前
前端组件通信新姿势:用mitt实现Toast组件的优雅通信
前端·react.js