520快到了,这个粒子爱心动画你应该用得上

说在前面

520快到了,又到一年一度画爱心的时候了,今天一起来实现一个粒子爱心动画,说不定你可以用得上😁

效果预览

码上掘金

https://code.juejin.cn/pen/7504587367895564327

码上掘金

CodePen

https://codepen.io/yongtaozheng/pen/GggzGmz

CodePen

代码实现

初始化

画布初始化
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
配置数据
let particles = [];  //粒子示例
let heartPoints = [];  //粒子坐标点
let centerX, centerY;//画布中心点
let animationId; 
let isAnimating = true;  //是否开启动画
const pointCount = 2000; // 粒子的数量
let flag = false;  //标记,判断粒子是否已经聚集到中心
const flagDistance = 3;  //粒子是否已经聚集中心的判断距离
let heartSize= Math.min(canvas.width, canvas.height) * 0.3;

生成爱心形状的点

爱心参数方程

直接用这个经典的心形曲线参数方程

function generateHeartPoints() {
  heartPoints = [];
  const points = pointCount; // 点的数量
  for (let i = 0; i < points; i++) {
    const t = (i * Math.PI * 2) / points;
    const x = 16 * Math.pow(Math.sin(t), 3);
    const y =
      13 * Math.cos(t) -
      5 * Math.cos(2 * t) -
      2 * Math.cos(3 * t) -
      Math.cos(4 * t);
    // 缩放并平移到中心
    heartPoints.push({
      x: centerX + (x * heartSize) / 30,
      y: centerY - (y * heartSize) / 30, // y坐标取负,因为Canvas的Y轴向下
    });
  }
}

t0 变化,生成连续的爱心点。

随机生成爱心点的颜色

预设几个粉色系的颜色,随机抽取其中的颜色作为新生成的粒子颜色。

function getRandomHeartColor() {
  const colors = [
    { r: 255, g: 64, b: 129 }, // 粉红色
    { r: 255, g: 105, b: 180 }, // 热粉色
    { r: 255, g: 20, b: 147 }, // 深粉色
    { r: 255, g: 192, b: 203 }, // 浅粉色
  ];
  return colors[Math.floor(Math.random() * colors.length)];
}

创建粒子类

构造函数
  • 初始位置随机分散在目标点附近

先看看不随机分散的效果

this.x = this.target.x;
this.y = this.target.y;

这样的效果明显不够粒子化,都成线条了,那么我们为了突出它的粒子性,我们可以将其坐标进行一个随机分散。

this.x = this.target.x + (Math.random() - 0.5) * heartSize * 0.3;
this.y = this.target.y + (Math.random() - 0.5) * heartSize * 0.3;

这样看起来效果是不是好点了?

  • 完整代码
constructor(targetPoint) {
  this.target = { ...targetPoint };
  this.basePoint = { ...targetPoint };
  this.x = this.target.x + (Math.random() - 0.5) * heartSize * 0.3;
  this.y = this.target.y + (Math.random() - 0.5) * heartSize * 0.3;
  // 速度和加速度
  this.vx = 0;
  this.vy = 0;
  this.ax = 0;
  this.ay = 0;
  // 粒子属性
  this.size = 1 + Math.random() * 2;
  this.alpha = 0.2 + Math.random() * 0.8;
  this.color = getRandomHeartColor();
  // 用于动画的随机相位
  this.phase = Math.random() * Math.PI * 2;
  this.pointsStack = [];
}
粒子跳动效果
  • 计算移动距离
const dx = this.target.x - this.x;
const dy = this.target.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
  • 物理特性模拟
// 模拟加速度(距离越远,加速度越大)
const force = distance * 0.0005;
this.ax = dx * force;
this.ay = dy * force;

// 添加一些随机性,使粒子有轻微的抖动
this.ax += (Math.random() - 0.5) * 0.1;
this.ay += (Math.random() - 0.5) * 0.1;

// 更新速度
this.vx += this.ax;
this.vy += this.ay;

// 添加阻力
this.vx *= 0.96;
this.vy *= 0.96;
  • 周期性改变目标点位置

正弦波生成器

//this.phase = Math.random() * Math.PI * 2
const pulseFactor =
    1 + 0.03 * Math.sin(Date.now() * 0.002 + this.phase);
this.target.x = centerX + (this.target.x - centerX) * pulseFactor;
this.target.y = centerY + (this.target.y - centerY) * pulseFactor;

时间驱动:Date.now()获取当前时间戳(毫秒),*0.002将时间单位转换为每 500ms 完成一次完整周期(频率=1/(2π/(0.002))≈0.318Hz

相位控制:phase为粒子个体相位差,避免所有粒子同步跳动,产生自然波动效果。例如相位差为 π/2 时,粒子运动轨迹相差 1/4 周期

这里有一个问题,在跳动过一段时间之后,粒子会聚集到中心点,跳动幅度就缩小在了中心点一个范围

那这里我们得来分析一下原因:

  • 1、 pulseFactor 的值域

我们都知道 sin 的值域是 [-1, 1] ,所以 pulseFactor 的取值范围是 [0.97, 1.03]

  • 2、偏移量的长期变化

设当前偏移量为 d = this.target.x - centerX,则更新后的偏移量为 d_new = d * pulseFactor

每次更新时,偏移量会被乘以 pulseFactor。由于 |pulseFactor - 1| < 0.03,即每次偏移量会以约 3% 的幅度衰减或放大。

由于 pulseFactor 围绕 1 波动,放大和缩小的幅度相近,因为缩小后想要变回原来大小需要的偏移量要更大,所以最终偏移量在反复缩放中逐渐衰减至接近 0

这里举个直白点的例子,比如现在有个值为 100,看下下面这两种场景:

  • (1)先放大再缩小

100 * 103% = 103

103 * 97% = 99.91

  • (2)先缩小再放大

100 * 97% = 97

97 * 103% = 99.91

通过上面两个场景,你发现了什么?

放大后再回归原本大小的概率 > 比变小后再回归原本大小的概率

所以偏移量在一个长期的运动过程后会逐渐衰减至接近 0

  • 3、解决偏移量趋于0的问题

这里我使用了一个简单的方法,就是先把趋于0的过程中的位置点都先保存起来,等到偏移量达到我们设定的一个偏小的阈值的时候,直接取出保存的点来做一个循环运动即可

保存粒子位置点

this.target.x = centerX + (this.target.x - centerX) * pulseFactor;
this.target.y = centerY + (this.target.y - centerY) * pulseFactor;
this.pointsStack.push({ ...this.target });

判断偏移量是否到达阈值

if (
  Math.abs(this.target.x - centerX) < flagDistance &&
  Math.abs(this.target.y - centerY) < flagDistance
) {
  flag = true;
}

取出保存的点进行循环运动

if (flag) {
  const point = this.pointsStack.pop();
  this.pointsStack.unshift(point);
  this.target = point;
  return;
}
粒子绘制
draw() {
    const gradient = ctx.createRadialGradient(
      this.x,
      this.y,
      0,
      this.x,
      this.y,
      this.size
    );

    // 设置渐变颜色
    gradient.addColorStop(
      0,
      `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.alpha})`
    );
    gradient.addColorStop(
      1,
      `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, 0)`
    );

    ctx.fillStyle = gradient;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
    ctx.fill();
  }
}

动画循环

function animate() {
  if (!isAnimating) return;
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 更新和绘制所有粒子
  particles.forEach((particle) => {
    particle.update();
    particle.draw();
  });

  animationId = requestAnimationFrame(animate);
}

源码

gitee

https://gitee.com/zheng_yongtao/video-code.git


  • 🌟 觉得有帮助的可以点个 star~
  • 🖊 有什么问题或错误可以指出,欢迎 pr~
  • 📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JYeontu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值