前言
情人节将至,想不想用代码给心仪的人准备一份特别的浪漫?今天就分享一个基于 EasyX 图形库的 C 语言心形粒子动画,粉紫色系的粒子会以心形为轨迹扩散、闪烁,还带有 "呼吸" 式的循环效果,既浪漫又能学习图形编程和粒子系统的基础逻辑,话不多说,直接上干货!
一、效果预览
运行代码后会弹出一个 1200×800 的窗口,粉紫色粒子会从中心开始,沿着心形轨迹逐渐扩散,颜色随粒子距离变化呈现渐变效果,同时有随机闪烁的粒子增加层次感,整体动画会以 "扩展→收缩→扩展" 的呼吸模式循环,按下任意键即可退出。
爱心代码运行结果
前提:一定要安装EasyX
二、代码核心讲解
2.1 基础原理:笛卡尔心形曲线(爱心的 "骨架")
2.1.1 核心数学原理
我们看到的爱心不是手绘的,而是基于经典笛卡尔心形参数方程生成的,这是整个动画的 "骨架":

- 参数 t:角度,取值范围 0≤t≤2π(对应 360°),遍历这个范围就能算出心形上所有点的坐标;
- 方程意义:通过三角函数的组合,让 (x,y) 坐标的轨迹自然形成对称的爱心轮廓;
- 为什么选这个方程:相比其他心形方程(如 (x2+y2−1)3−x2y3=0),参数方程更易遍历、更易控制粒子密度。
2.1.2 代码实现与优化
cpp
// 生成原始心形点集(笛卡尔心形参数方程)
int index = 0;
double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
for (double radian = 0.1; radian <= 2 * PI; radian += 0.005)
{
x2 = 16 * pow(sin(radian), 3);
y2 = 13 * cos(radian) - 5 * cos(2 * radian) - 2 * cos(3 * radian) - cos(4 * radian);
// 按间距筛选点,避免粒子过于密集
double distance = sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
if (distance > average_distance)
{
x1 = x2, y1 = y2;
origin_points[index].x = x2;
origin_points[index++].y = y2;
}
}
- 关键优化 :遍历角度时,通过
distance > average_distance筛选点 ------ 如果相邻两个点的距离小于阈值,就跳过,避免粒子扎堆,让爱心轮廓更均匀; - 存储逻辑 :筛选后的点存入
origin_points数组,作为后续粒子扩散的 "基础模板"。
2.2 图形学原理:坐标系转换(让爱心显示在屏幕中央)
2.2.1 核心矛盾
- 数学坐标系:原点在中心,Y 轴向上(符合我们对 "上下" 的直觉);
- 屏幕坐标系:原点在左上角,Y 轴向下(计算机绘图的默认规则)。
如果直接用数学坐标绘图,爱心会 "倒过来" 且偏离屏幕中心,因此必须做坐标转换。
2.2.2 转换公式与代码
cpp
// 数学坐标 → 屏幕X坐标:原点移到屏幕中心
double screen_x(double x)
{
return x + xScreen / 2;
}
// 数学坐标 → 屏幕Y坐标:Y轴翻转 + 原点下移
double screen_y(double y)
{
return -y + yScreen / 2;
}
- X 轴转换:所有点的 X 坐标加上屏幕宽度的一半,把爱心从 "数学中心(0,0)" 移到 "屏幕中心(600,400)";
- Y 轴转换:先对 y 取反(翻转 Y 轴),再加上屏幕高度的一半(下移到屏幕中心),最终让爱心 "正过来" 且居中。
2.3 动画原理 1:粒子系统(让爱心 "扩散")
2.3.1 粒子系统的核心逻辑
粒子系统是实现 "扩散爱心" 的关键,核心是:以原始心形点集为模板,生成多层、带随机扰动、颜色渐变的粒子,模拟 "从中心向外扩散" 的效果。
2.3.2 关键子原理拆解
(1)概率衰减:让外圈粒子更稀疏
cpp
double success_p = 1 / (1 + pow(e, 8 - size / 2));//概率衰减曲线
- 公式意义 :这是基于自然常数 e 的 S 型衰减函数(Logistic 函数),
size越大(粒子越靠外),success_p越小,生成粒子的概率越低; - 视觉效果:外圈粒子稀疏、内圈密集,符合 "扩散时能量衰减" 的直觉,避免爱心边缘过于生硬。
(2)颜色渐变:让粒子有 "层次感"
cpp
if (lightness > 1)
{
lightness -= 0.0025; // 亮度逐渐降低
}
points[index].color = RGB(
GetRValue(color) / lightness,
GetGValue(color) / lightness,
GetBValue(color) / lightness
);
- 原理 :
lightness初始为 1.4(亮度高),随着size增大(粒子外扩),亮度逐渐降低; - 实现 :通过
GetRValue/GetGValue/GetBValue提取颜色的 RGB 分量,除以亮度值,让颜色从亮粉色逐渐过渡到深紫色,模拟 "光晕衰减"。
(3)随机扰动:让粒子更 "灵动"
cpp
points[index].x = size * origin_points[i].x + create_random(-4, 4);
points[index++].y = size * origin_points[i].y + create_random(-4, 4);
- 原理:在 "放大后的原始点坐标" 基础上,加 ±4 的随机偏移;
- 效果:粒子不会严格按心形轨迹排列,而是有轻微抖动,模拟真实粒子的无规则运动,避免动画过于机械。
2.4 动画原理 2:帧动画与呼吸效果(让爱心 "动起来")
2.4.1 预渲染帧:保证动画流畅
cpp
// 生成各帧动画
for (int frame = 0; frame < frames; ++frame)
{
images[frame] = IMAGE(xScreen, yScreen);
SetWorkingImage(&images[frame]);
// 更新粒子位置、绘制粒子、添加闪烁粒子...
}
- 原理 :提前计算 20 帧(
frames=20)的画面,存入images数组(帧缓冲区); - 好处:播放时无需实时计算粒子位置,只需直接显示预渲染的帧,避免卡顿(如果实时计算,大量粒子的坐标更新会导致动画掉帧)。
2.4.2 呼吸效果:扩展→收缩循环
cpp
bool extend = true, shrink = false;
for (int frame = 0; !_kbhit();)//按任意键退出
{
putimage(0, 0, &images[frame]);//显示当前帧
// 帧序号更新逻辑
if (extend)
{
if (frame == 19) { shrink = true; extend = false; }
else { ++frame; } // 扩展:帧序号递增(从0到19)
}
else
{
if (frame == 0) { shrink = false; extend = true; }
else { --frame; } // 收缩:帧序号递减(从19到0)
}
}
- 原理:通过控制帧序号的 "递增→递减→递增" 循环,依次显示 "粒子从中心扩散→粒子收缩回中心" 的画面;
- 效果 :视觉上形成爱心 "呼吸" 的动态感,20ms / 帧(
Sleep(20))的间隔保证动画速度适中。
2.5 视觉优化:随机闪烁粒子(增强氛围感)
cpp
// 添加随机闪烁粒子
for (double size = 17; size < 23; size += 0.3)
{
for (index = 0; index < quantity; ++index)
{
// 外圈粒子闪烁概率更高,内圈更低
if ((create_random(0, 100) / 100.0 > 0.6 && size >= 20) || (size < 20 && create_random(0, 100) / 100.0>0.95))
{
// 绘制随机位置的闪烁粒子
}
}
}
- 原理:在爱心外圈(size≥20)设置 60% 的闪烁概率,内圈(size<20)仅 5% 的概率,模拟 "星光闪烁";
- 效果:增加动画的层次感和浪漫感,避免单一的粒子扩散显得单调。
三、完整可运行代码
cpp
#include<graphics.h>
#include<conio.h>
#include<time.h>
#include<math.h>
#include<stdlib.h>
//定义点结构体,包含坐标和颜色
struct Point
{
double x, y;
COLORREF color;
};
//预设颜色数组(粉紫色系)
COLORREF colors[256] = {
RGB(255, 105, 180),//热粉色
RGB(218, 112, 214),//紫红色
RGB(255, 182, 193),//浅粉色
RGB(238, 130, 238),//紫罗兰色
RGB(255, 20, 147),// 深粉色
RGB(199, 21, 133),//中等紫罗兰红色
RGB(255, 160, 122)//浅珊色(增加层次感)
};
//屏幕尺寸
const int xScreen = 1200;
const int yScreen = 800;
// 数学常数
const double PI = 3.1415926535;
const double e = 2.71828;
//粒子系统参数
const double average_distance = 0.162;//粒子平均间距
const int quantity = 506;//基础粒子数量
const int circles = 210;// 扩散圈数
const int frames = 20;//动画帧数
//存储点集的组数
Point origin_points[quantity];//原始心形点集
Point points[circles * quantity];//动态粒子点集
IMAGE images[frames];//动态帧缓冲区
//坐标系转换:数学坐标->屏幕坐标
double screen_x(double x)
{
return x + xScreen / 2;//将圆点移到屏幕中心
}
double screen_y(double y)
{
return -y + yScreen / 2;//Y轴翻转
}
//生成指定范围的随机数
int create_random(int x1, int x2)
{
if (x2 > x1)
return rand() % (x2 - x1 + 1) + x1;
return x1;//处理x2<=x1的情况,避免未定义行为
}
//创建粒子数据并生成动画帧
void create_data()
{
//生成原始心形点集(参数方程:笛卡尔心形曲线)
int index = 0;
double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
for (double radian = 0.1; radian <= 2 * PI; radian += 0.005)
{
x2 = 16 * pow(sin(radian), 3);
y2 = 13 * cos(radian) - 5 * cos(2 * radian) - 2 * cos(3 * radian) - cos(4 * radian);
//根据间距筛选点
double distance = sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
if (distance > average_distance)
{
x1 = x2, y1 = y2;
origin_points[index].x = x2;
origin_points[index++].y = y2;
}
}
//生成动态粒子(多次扩散效果)
index = 0;
for (double size = 0.1, lightness = 1.4; size <= 20; size += 0.1)
{
double success_p = 1 / (1 + pow(e, 8 - size / 2));//概率衰减曲线
if (lightness > 1)
{
lightness -= 0.0025;
}
for (int i = 0; i < quantity; ++i)
{
if (success_p > create_random(0, 100) / 100.0)
{
COLORREF color = colors[create_random(0, 6)];
//颜色亮度调整
points[index].color = RGB(
GetRValue(color) / lightness,
GetGValue(color) / lightness,
GetBValue(color) / lightness
);
//添加随机扰动
points[index].x = size * origin_points[i].x + create_random(-4, 4);
points[index++].y = size * origin_points[i].y + create_random(-4, 4);
}
}
}
int points_size = index;
//生成各帧动画
for (int frame = 0; frame < frames; ++frame)
{
images[frame] = IMAGE(xScreen, yScreen);
SetWorkingImage(&images[frame]);
//更新粒子位置并绘制
for (index = 0; index < points_size; ++index)
{
//计算粒子运动(抛物线速度)
double x = points[index].x, y = points[index].y;
double distance = sqrt(x * x + y * y);
double distance_increase = -0.0009 * distance * distance + 0.35714 * distance + 5;
double x_increase = distance_increase * x / distance / frames;
double y_increase = distance_increase * y / distance / frames;
points[index].x += x_increase;
points[index].y += y_increase;
//绘制粒子
setfillcolor(points[index].color);
solidcircle((int)screen_x(points[index].x), (int)screen_y(points[index].y), 1);
}
//添加随机闪烁粒子
for (double size = 17; size < 23; size += 0.3)
{
for (index = 0; index < quantity; ++index)
{
if ((create_random(0, 100) / 100.0 > 0.6 && size >= 20) || (size < 20 && create_random(0, 100) / 100.0>0.95))
{
double x, y;
//大粒子添加更大幅度扰动
if (size >= 20)
{
x = origin_points[index].x * size + create_random(-frame * frame / 5 - 15, frame * frame / 5 + 15);
y = origin_points[index].y * size + create_random(-frame * frame / 5 - 15, frame * frame / 5 + 15);
}
else
{
x = origin_points[index].x * size + create_random(-5, 5);
y = origin_points[index].y * size + create_random(-5, 5);
}
setfillcolor(colors[create_random(0, 6)]);
solidcircle((int)screen_x(x), (int)screen_y(y), 1);
}
}
}
}
SetWorkingImage();//恢复默认绘图目标
}
int main()
{
initgraph(xScreen, yScreen);//初始化图形窗口
BeginBatchDraw();//开始批量绘制
srand((unsigned int)time(0));//随机数种子
create_data();//生成所有动画数据
//动画循环(呼吸效果)
bool extend = true, shrink = false;
for (int frame = 0; !_kbhit();)//按任意键退出
{
putimage(0, 0, &images[frame]);//显示当前帧
FlushBatchDraw();//刷新绘制
Sleep(20);//控制帧率
cleardevice();//清空画面
//更新帧序号(先扩展后收缩)
if (extend)
{
if (frame == 19)
{
shrink = true;
extend = false;
}
else
{
++frame;
}
}
else
{
if (frame == 0)
{
shrink = false;
extend = true;
}
else
{
--frame;
}
}
}
EndBatchDraw();//结束批量绘制
closegraph();//关闭图形窗口
return 0;
}
如果本篇内容对你有帮助,欢迎点赞、收藏、关注!有任何疑问,评论区留言交流~