Canvas动态爱心粒子动画实现

现在将介绍一个基于 Canvas 的动态爱心粒子动画实现,主要功能是生成一个由粒子组成的、会周期性脉动的爱心轮廓,粒子会沿着轨迹移动并留下拖尾效果。以下是详细的技术分析:

​一、整体结构与核心依赖​

  • ​HTML​​:仅包含一个 <canvas> 元素作为绘图容器,无其他DOM元素。
  • ​CSS​​:设置 canvas 为全屏覆盖,背景为半透明黑色(rgba(0,0,0,.2))。
  • ​JavaScript​​:核心逻辑集中在 Canvas 绘图、粒子系统控制和动画循环。

​二、代码

<!DOCTYPE html>

<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

    <title>💗</title>
    <style>
        canvas {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, .2);
        }
    </style>
</head>

<body>

    <canvas id="heart" width="1920" height="947"></canvas>
    <script>
        window.requestAnimationFrame =
            window.__requestAnimationFrame ||
            window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            window.msRequestAnimationFrame ||
            (function () {
                return function (callback, element) {
                    var lastTime = element.__lastTime;
                    if (lastTime === undefined) {
                        lastTime = 0;
                    }
                    var currTime = Date.now();
                    var timeToCall = Math.max(1, 33 - (currTime - lastTime));
                    window.setTimeout(callback, timeToCall);
                    element.__lastTime = currTime + timeToCall;
                };
            })();
        window.isDevice = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(((navigator.userAgent || navigator.vendor || window.opera)).toLowerCase()));
        var loaded = false;
        var init = function () {
            if (loaded) return;
            loaded = true;
            var mobile = window.isDevice;
            var koef = mobile ? 0.5 : 1;
            var canvas = document.getElementById('heart');
            var ctx = canvas.getContext('2d');
            var width = canvas.width = koef * innerWidth;
            var height = canvas.height = koef * innerHeight;
            var rand = Math.random;
            ctx.fillStyle = "rgba(0,0,0,1)";
            ctx.fillRect(0, 0, width, height);

            var heartPosition = function (rad) {
                //return [Math.sin(rad), Math.cos(rad)];
                return [Math.pow(Math.sin(rad), 3), -(15 * Math.cos(rad) - 5 * Math.cos(2 * rad) - 2 * Math.cos(3 * rad) - Math.cos(4 * rad))];
            };
            var scaleAndTranslate = function (pos, sx, sy, dx, dy) {
                return [dx + pos[0] * sx, dy + pos[1] * sy];
            };

            window.addEventListener('resize', function () {
                width = canvas.width = koef * innerWidth;
                height = canvas.height = koef * innerHeight;
                ctx.fillStyle = "rgba(0,0,0,1)";
                ctx.fillRect(0, 0, width, height);
            });

            var traceCount = mobile ? 20 : 50;
            var pointsOrigin = [];
            var i;
            var dr = mobile ? 0.3 : 0.1;
            for (i = 0; i < Math.PI * 2; i += dr) pointsOrigin.push(scaleAndTranslate(heartPosition(i), 210, 13, 0, 0));
            for (i = 0; i < Math.PI * 2; i += dr) pointsOrigin.push(scaleAndTranslate(heartPosition(i), 150, 9, 0, 0));
            for (i = 0; i < Math.PI * 2; i += dr) pointsOrigin.push(scaleAndTranslate(heartPosition(i), 90, 5, 0, 0));
            var heartPointsCount = pointsOrigin.length;

            var targetPoints = [];
            var pulse = function (kx, ky) {
                for (i = 0; i < pointsOrigin.length; i++) {
                    targetPoints[i] = [];
                    targetPoints[i][0] = kx * pointsOrigin[i][0] + width / 2;
                    targetPoints[i][1] = ky * pointsOrigin[i][1] + height / 2;
                }
            };

            var e = [];
            for (i = 0; i < heartPointsCount; i++) {
                var x = rand() * width;
                var y = rand() * height;
                e[i] = {
                    vx: 0,
                    vy: 0,
                    R: 2,
                    speed: rand() + 5,
                    q: ~~(rand() * heartPointsCount),
                    D: 2 * (i % 2) - 1,
                    force: 0.2 * rand() + 0.7,
                    f: "hsla(240," + ~~(40 * rand() + 60) + "%," + ~~(60 * rand() + 20) + "%,.3)",   //修改颜色
                    trace: []
                };
                for (var k = 0; k < traceCount; k++) e[i].trace[k] = { x: x, y: y };
            }

            var config = {
                traceK: 0.4,
                timeDelta: 0.01
            };

            var time = 0;
            var loop = function () {
                var n = -Math.cos(time);
                pulse((1 + n) * .5, (1 + n) * .5);
                time += ((Math.sin(time)) < 0 ? 9 : (n > 0.8) ? .2 : 1) * config.timeDelta;
                ctx.fillStyle = "rgba(0,0,0,.1)";
                ctx.fillRect(0, 0, width, height);
                for (i = e.length; i--;) {
                    var u = e[i];
                    var q = targetPoints[u.q];
                    var dx = u.trace[0].x - q[0];
                    var dy = u.trace[0].y - q[1];
                    var length = Math.sqrt(dx * dx + dy * dy);
                    if (10 > length) {
                        if (0.95 < rand()) {
                            u.q = ~~(rand() * heartPointsCount);
                        } else {
                            if (0.99 < rand()) {
                                u.D *= -1;
                            }
                            u.q += u.D;
                            u.q %= heartPointsCount;
                            if (0 > u.q) {
                                u.q += heartPointsCount;
                            }
                        }
                    }
                    u.vx += -dx / length * u.speed;
                    u.vy += -dy / length * u.speed;
                    u.trace[0].x += u.vx;
                    u.trace[0].y += u.vy;
                    u.vx *= u.force;
                    u.vy *= u.force;
                    for (k = 0; k < u.trace.length - 1;) {
                        var T = u.trace[k];
                        var N = u.trace[++k];
                        N.x -= config.traceK * (N.x - T.x);
                        N.y -= config.traceK * (N.y - T.y);
                    }
                    ctx.fillStyle = u.f;
                    for (k = 0; k < u.trace.length; k++) {
                        ctx.fillRect(u.trace[k].x, u.trace[k].y, 1, 1);
                    }
                }
                ctx.fillStyle = "rgba(255,255,255,1)";
                for (i = u.trace.length + 13; i--;) ctx.fillRect(targetPoints[i][0], targetPoints[i][1], 2, 2);

                window.requestAnimationFrame(loop, canvas);
            };
            loop();
        };

        var s = document.readyState;
        if (s === 'complete' || s === 'loaded' || s === 'interactive') init();
        else document.addEventListener('DOMContentLoaded', init, false);
    </script>

</body>

</html>

三、关键技术点解析​

​1. Canvas 初始化与自适应​
  • ​画布尺寸​​:初始设置 width="1920" height="947",但通过 JavaScript 动态调整为窗口实际尺寸(koef * innerWidth/Height),确保全屏显示。
  • ​重绘处理​​:窗口 resize 时,重新设置画布尺寸并清空背景,避免画面拉伸变形。
​2. 爱心轮廓的数学生成​
  • ​心形参数方程​​:通过 heartPosition 函数定义心形的极坐标方程,转换为笛卡尔坐标:
    return [Math.pow(Math.sin(rad), 3), -(15 * Math.cos(rad) - 5 * Math.cos(2 * rad) - 2 * Math.cos(3 * rad) - Math.cos(4 * rad))];
    该方程生成的点经缩放(scaleAndTranslate)后,形成居中的爱心轮廓。
  • ​多层级轮廓​​:通过三次不同缩放(210、150、90)生成三层爱心轮廓,增加细节层次。
​3. 粒子系统设计​
  • ​粒子属性​​:每个粒子(对象 e[i])包含位置(trace 数组记录轨迹)、速度(vx/vy)、大小(R)、运动参数(speedforce)、颜色(f)等。
  • ​轨迹拖尾​​:通过 trace 数组记录粒子历史位置(长度由 traceCount 控制),绘制时从旧到新逐点绘制,形成逐渐消失的拖尾效果。
  • ​目标追踪​​:粒子会向随机选择的心形轮廓点(targetPoints)移动,通过计算粒子与目标点的距离调整速度方向(类似“引力”效果)。
​4. 动态脉动效果​
  • ​时间控制​​:通过 time 变量和余弦函数(Math.cos(time))控制心形的周期性收缩与膨胀:
    pulse((1 + n) * .5, (1 + n) * .5); // n = -Math.cos(time)
    pulse 函数调整心形轮廓点的缩放比例,使爱心呈现“呼吸感”。
​5. 性能优化​
  • ​设备适配​​:通过 window.isDevice 检测移动设备,减少粒子数量(traceCount)和运动步长(dr),降低计算压力。
  • ​绘制优化​​:使用 ctx.fillRect(x, y, 1, 1) 绘制单像素粒子,比绘制圆形更高效;背景用半透明黑色覆盖(rgba(0,0,0,.1)),利用“残影”实现拖尾效果,避免频繁清空画布。

​四、视觉效果总结​

  • ​核心元素​​:动态脉动的爱心轮廓 + 沿轨迹移动的粒子拖尾。
  • ​颜色风格​​:粒子颜色为随机蓝紫色(hsla(240, ..., ..., .3)),背景为深色,突出粒子轨迹。
  • ​动态表现​​:爱心周期性收缩膨胀,粒子不断向轮廓点移动并更新轨迹,形成流动的“星光”效果。

​总结​

主要通过 Canvas 和粒子系统实现了一个简洁而优雅的动态爱心动画,适合作为情感类网页(如情侣空间、婚礼网站)的背景或装饰元素。核心亮点在于利用数学方程生成心形轮廓、粒子的轨迹拖尾效果,以及自适应不同设备的性能优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值