制作粒子扩散的点赞动效果

14,217 阅读2分钟

话不多说, 让我们先来看看效果.

屏幕录制2022-02-06 下午9.gif

其他教程

首先说说市面上其他教程大部分都是使用图片制作这类效果, 个人认为这样缺少了可扩展性可玩性.

文章简介

  1. 本文将使用js制作带有粒子效果的点赞动效.
  2. 本文语法环境为vue3.2.

🖖🖖🖖 让我们开始吧 🖖🖖🖖

svg 爱心图片

这里之所以选择svg图片, 是因为: svg可以通过css以及js修改图片中的内容, 比如给svg图片的某一部分填充颜色.

SVG是一种可缩放的矢量图像文件格式, 它使用xml格式的文件来描述图像. 因此可以很方便的嵌入HTML页面中, 并且我们可以通过cssjs控制调整svg图片.
SVG在W3C上的介绍 首先我们要会在前端添加图标字体或者图片, 这里我们可以选择使用国内常用的阿里巴巴图标矢量库

下载svg格式素材的步骤

  1. 按关键词搜索图标

截屏2022-02-06 下午10.52.20.png

  1. 找到喜欢的图标点击下载按钮(下载按钮在鼠标悬停后出现)

截屏2022-02-06 下午10.52.31.png

  1. 点击 复制SVG代码 完成svg图片的下载

截屏2022-02-06 下午10.52.39.png

  1. 直接将我们复制的代码粘贴入代码中
<template>
    <div id="app">
        <!-- 这里是我们刚刚复制的svg代码 -->
        <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
            <path d="M533.504 268.288q33.792-41.984 71.68-75.776 32.768-27.648 74.24-50.176t86.528-19.456q63.488 5.12 105.984 30.208t67.584 63.488 34.304 87.04 6.144 99.84-17.92 97.792-36.864 87.04-48.64 74.752-53.248 61.952q-40.96 41.984-85.504 78.336t-84.992 62.464-73.728 41.472-51.712 15.36q-20.48 1.024-52.224-14.336t-69.632-41.472-79.872-61.952-82.944-75.776q-26.624-25.6-57.344-59.392t-57.856-74.24-46.592-87.552-21.504-100.352 11.264-99.84 39.936-83.456 65.536-61.952 88.064-35.328q24.576-5.12 49.152-1.536t48.128 12.288 45.056 22.016 40.96 27.648q45.056 33.792 86.016 80.896z" p-id="2432"></path>
        </svg>
    <div>
</template>

学会使用css编辑SVG图片

这里简单介绍一下如何使用css编辑SVG图片

<!-- 添加class属性 -->
...
    <svg class="mySvg">
        ...
    </svg>
...
// 使用class选择器
.mySvg{
    // 将svg填充成红色
    fill: red;
    // 给svg添加边框
    stroke: $Gray;      // 边框颜色
    stroke-width: 80px; // 边框粗细
}

制作粒子扩散效果

屏幕录制2022-02-06 下午8.gif 这里我使用了一个轻量级的js图形动画库.

  1. 首先我们安装mo.js
npm install @mojs/core
  1. 在页面中引入
// 引入 mojs 模块
import mojs from '@mojs/core';
<script>
export default {
    setup(){
        // 使用 mojs
        new mojs.Timeline()
    }
}
</script>

本文使用的版本为 "@mojs/core": "^1.3.0"

  1. 这里心形外面扩散的效果在 mo.js 中叫做 Burst中文含义“爆裂”, 根据官网我们可以完成以下代码的编写.
const heart = ref(null);
...
new mojs.Burst({
    // 爆裂范围 {从多大 : 到多大}
    radius: {0: 50},
    // 动画挂载的父元素, 如果不填默认挂载到 <body>
    parent: heart.value,
    // 动画延迟的贝塞尔曲线函数
    easing:  mojs.easing.bezier(0.1, 1, 0.3, 1),
    // 动画延迟时间
    duration: 1500,
    // 在动画动之前等待的时间 (这里一般设置150ms方便减少低端机型可能会存在的卡顿)
    delay: 300,
    // 扩散的粒子配置
    children: {
        duration: 750,
        // 粒子大小变换 {从多大 : 到多大}
        // rand(from, to) rand函数可以帮我们随机出一个区间的值
        radius: {0: 'rand(5, 25)'},
        // 形状选择, 这里我们选择了 “圆形”
        shape: 'circle',
        // 粒子可选的填充色
        fill: ['#1abc9c', '#2ecc71', '#00cec9', '#3498db', '#9b59b6', '#fdcb6e', '#f1c40f', '#e67e22', '#e74c3c', '#e84393']
    },
    // 透明度
    opacity: 0.6,
    // 生成的粒子数量
    count: 10,
    onStart(){
        // 动画触发前的钩子函数     
    },
    onComplete(){
        // 动画完成后的钩子函数     
    }
}).play();

这里需要注意的是, 最后需要调用 .play() 方法才能使动画动起来

屏幕录制2022-02-06 下午11.gif

mojs给我们提供了很多形状动画效果, 甚至可以控制动画播放的进度, 这里不一一介绍, 如果各位大佬有问题可以在评论区讨论.

制作红星外的红晕

屏幕录制2022-02-06 下午11 (1).gif

这里其实是绘制了一个空心圆形

const heart_icon = ref(null);
...
new mojs.Transit({
    // 动画挂载的父元素, 如果不填默认挂载到 <body>
    parent: heart.value,
    // 动画延迟时间
    duration: 750,
    // 图形的类型, 这里选择圆形
    type: 'circle',
    // 半径 {从多大 : 到多大}
    radius: { 0: 20 },
    // 填充透明色
    fill: 'transparent',
    // 边框颜色
    stroke: '#E05B5B',
    // 边框粗细 {从多粗 : 到多粗}
    strokeWidth: { 20: 0 },
    // 透明度
    opacity: 0.6,
    // 动画延迟的贝塞尔曲线函数
    easing: mojs.easing.bezier(0, 1, 0.5, 1)
})

制作点赞状态锁

这里的锁主要是用来预防频繁的播放动画, 只有在未点赞的状态过渡到点赞状态才需要播放动画.

<template>
    ...
        <svg :style="heartStyle" @click="thumbsUp">
            ...
        </svg>
    ...
</template>

<script>
import { ref, computed, onMounted } from 'vue'
import mojs from '@mojs/core';
export default { 
    setup(){
        // 是否点赞
        const hearted = ref(false);
        
        // 点赞
        function thumbsUp(){
            if(!hearted.value){
                new mojs.Burst({
                    onStart(){
                        // ---------------- 发送点赞请求----------------
                        hearted.value = true
                    },
                }).play();
            }else{
                // 取消赞
                hearted.value = false
            }
        }
        
        // 爱心样式
        const heartStyle = computed(()=>{
            return {
                fill: `${hearted.value?'#E05B5B':''}`,
                stroke: `${hearted.value?'#E05B5B':''}`
            }
        })

        return {
            hearted,
            thumbsUp
        }
    }
}
</script>

mojs 中的时间线

mojs中的Timeline对象主要是用于处理同一时间需要同时播放的动画.

// 红晕
let aperture = new mojs.Transit({...})
// 粒子扩散
let burst = new mojs.Burst({...})
// 爱心弹跳效果
let bounce = new mojs.Tween({...})

// 添加动画到同一时间线播放
new mojs.Timeline().add(burst, aperture, bounce).play();

加上亿点细节后就做好啦

屏幕录制2022-02-06 下午8.gif

全部源码

<template>
    <div class="oper-item" @click="thumbsUp">
        <div class="heart" ref="heart">
            <svg ref="heart_icon" :style="heartStyle" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"><path d="M533.504 268.288q33.792-41.984 71.68-75.776 32.768-27.648 74.24-50.176t86.528-19.456q63.488 5.12 105.984 30.208t67.584 63.488 34.304 87.04 6.144 99.84-17.92 97.792-36.864 87.04-48.64 74.752-53.248 61.952q-40.96 41.984-85.504 78.336t-84.992 62.464-73.728 41.472-51.712 15.36q-20.48 1.024-52.224-14.336t-69.632-41.472-79.872-61.952-82.944-75.776q-26.624-25.6-57.344-59.392t-57.856-74.24-46.592-87.552-21.504-100.352 11.264-99.84 39.936-83.456 65.536-61.952 88.064-35.328q24.576-5.12 49.152-1.536t48.128 12.288 45.056 22.016 40.96 27.648q45.056 33.792 86.016 80.896z" p-id="2432"></path></svg>
        </div>
        <span class="oper-text">120 喜欢</span>
    </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue'
import mojs from '@mojs/core';
export default {
    name: 'ZcCardArticle',
    setup(){
        // 是否点赞
        const hearted = ref(false);
        // 点赞按钮
        const heart = ref(null);
        const heartP = ref(1);
        const heartStyle = computed(()=>{
            return {
                fill: `${hearted.value?'#E05B5B':''}`,
                stroke: `${hearted.value?'#E05B5B':''}`,
                transform: `scale3d(${heartP.value},${heartP.value},1)`
            }
        })
        // 红心本体
        const heart_icon = ref(null);
        // 扩散 
        let burst
        // 红色光圈
        let aperture
        // 弹跳
        let bounce
        onMounted(()=>{
            aperture = new mojs.Transit({
                parent: heart.value,
                duration: 750,
                type: 'circle',
                radius: { 0: 20 },
                fill: 'transparent',
                stroke: '#E05B5B',
                strokeWidth: { 20: 0 },
                opacity: 0.6,
                isRunLess: true,
                easing: mojs.easing.bezier(0, 1, 0.5, 1)
            })

            burst = new mojs.Burst({
                    radius: {0: 50},
                    parent: heart.value,
                    easing:  mojs.easing.bezier(0.1, 1, 0.3, 1),
                    // duration: 1500,
                    delay: 300,
                    children: {
                        duration: 750,
                        radius: {0: 'rand(5, 25)'},
                        shape: ['circle','rect','polygon'],
                        fill: ['#1abc9c', '#2ecc71', '#00cec9', '#3498db', '#9b59b6', '#fdcb6e', '#f1c40f', '#e67e22', '#e74c3c', '#e84393'],
                        degreeShift: 'rand(-90, 90)',
                        delay: 'stagger(0, 40)',
                    },
                    opacity: 0.6,
                    count: 10,
                    onStart(){
                        // 发送点赞请求
                        hearted.value = true
                       
                    },
                    onComplete(){

                    }
                });
            bounce = new mojs.Tween({
                    duration: 1200,
                    onUpdate: function(progress) {
                        if (progress > 0.3) {
                        heartP.value = mojs.easing.elastic.out(1.43 * progress - 0.43);
                        // heart_icon.value.style.transform = 'scale3d(' + elasticOutProgress + ',' + elasticOutProgress + ',1)';
                    } else {
                        heartP.value = 0
                        // heart_icon.value.style.transform = 'scale3d(0,0,1)';
                    }
                }
            })
        })

        // 点赞
        function thumbsUp(){
            if(!hearted.value){
                new mojs.Timeline().add(burst, aperture, bounce).play();
            }else{
                // 取消赞
                hearted.value = false
            }
        }
        return {
            hearted,
            heart,
            heartStyle,
            heart_icon,
            burst,
            thumbsUp,
            aperture,
            bounce,
            heartP
        }
    }
}
</script>

<style lang="scss" scoped>
.oper-item{
    display: flex;
    align-items: center;
    justify-content: center;
    width: 33%;
    height: 24px;
    border-right: 1px solid rgba(#9A9DAA,.3);
    cursor: pointer;
}
.oper-item:last-child{
    border: none;
}
.oper-item i{
    color: #9A9DAA;
    font-size: 18px;
}
.oper-item .heart{
    height: 20px;
    position: relative;
    display: inline-block;
}
.oper-item .heart svg{
    transition: fill .3s, stroke .3s;
    stroke: #9A9DAA;
    stroke-width: 80px;
    fill: transparent;
}
.oper-item:hover .heart svg{
    stroke: #E05B5B;
}
.oper-text{
    margin-left: 12px;
    font-size: 13px;
    user-select: none;
}
</style>

可能会出现的问题 (持续更新)

1. 使ref定位元素配置 parent: heart.value可能会出现 undefined , 在 onMounted 中创建动画即可.

2.如果您想在IE11甚至更老的浏览上使用mo.js, 试试使用 npm i @mojs/core@0.288.2这个版本.

这条测试由掘金用户 行动派_ 提供, 非常感谢大佬的学术交流.

您的点赞是我更文的最大动力, 点赞数越多就会更有动力更新 🤣🤣🤣.