介绍
在这个项目中,你将学习如何使用 C 编程语言创建一个迷人的动态心形动画。该项目利用 X Window 系统来渲染动画视觉效果。按照逐步说明,你将设置项目、生成数据,并创建一个引人入胜的动画,让动态的心形在屏幕上栩栩如生。
👀 预览

🎯 任务
在这个项目中,你将学习:
- 如何设置一个 C 编程项目来创建动态心形动画
- 如何使用 X Window 系统库来创建和管理图形窗口
- 如何生成随机点并将它们动画化为心形
- 如何控制动画进行扩展和收缩,创造出引人入胜的视觉效果
🏆 成果
完成这个项目后,你将能够:
- 在 C 语言中使用 X Window 系统库进行图形编程
- 在 C 语言中生成和操作随机点
- 结合数据生成和渲染技术创建动态动画
创建项目文件
确保你已安装所需的库。你需要 X11 开发库。你可以使用以下命令安装它们:
sudo apt update
sudo apt-get install libx11-dev
然后,创建一个名为dynamic_heart.c的新文件,并在你喜欢的代码编辑器中打开它。
cd ~/project
touch dynamic_heart.c
定义必要的变量
首先,我们需要编写 C 代码。第一步是包含头文件:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#include <stdbool.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <unistd.h>
定义用于存储点信息的结构体,包括其 x 坐标、y 坐标和颜色。用于在图形中表示点。
struct Point {
double x, y;
unsigned long color;
};
然后,定义一些全局变量:
unsigned long colors[7] = {0xff1f53, 0xfcdefa, 0xff0000, 0xff0000, 0xff0202, 0xff0008, 0xff0505};
const int xScreen = 1200;
const int yScreen = 800;
const double PI = 3.1426535159;
const double e = 2.71828;
const double average_distance = 0.162;
const int quantity = 506;
const int circles = 210;
const int frames = 20;
struct Point* origin_points;
struct Point* points;
使用与 X Window 系统相关的变量来创建和管理图形窗口及图形上下文。
Display *display;
Window win;
GC gc;
display表示 X 服务器连接,win表示窗口,gc表示图形上下文,用于绘制图形元素。
实现屏幕坐标函数
double screen_x(double x) {
x += xScreen / 2;
return x;
}
double screen_y(double y) {
y = -y + yScreen / 2;
return y;
}
screen_x和screen_y函数的目的是将实际坐标映射到屏幕坐标,以确保动画中的图形元素在屏幕的可见区域内,并显示在正确的位置。具体来说:
screen_x:
- 此函数将实际的 x 坐标值作为参数,然后对其进行调整,以便在屏幕上正确绘制。它将输入的 x 坐标值加上
xScreen的一半,这样它将位于屏幕的水平中心,因为xScreen表示屏幕的宽度,除以2可找到屏幕的水平中心。 - 该函数返回调整后的 x 坐标值,用于在屏幕上绘制点或形状。
screen_y:
- 此函数与
screen_x类似,但处理 y 坐标。它将实际的 y 坐标值作为参数,并将其转换为屏幕坐标系。首先,它反转 y 坐标,使坐标系的原点位于屏幕的左上角,然后将其加上yScreen的一半,以便坐标位于屏幕的垂直中心。 - 该函数返回调整后的 y 坐标值,以便在屏幕上正确绘制点或形状。
实现随机数生成器函数
int create_random(int x1, int x2) {
if (x2 > x1) {
return rand() % (x2 - x1 + 1) + x1;
}
return 0;
}
这是一个用于在指定范围内生成随机整数的函数,它用于使生成点的颜色和位置随机化,以增加动画的视觉多样性。
其中x1和x2是作为参数传递的两个整数。该函数返回x1和x2之间的一个随机整数,包括x1和x2。如果x2大于x1,则使用rand()函数生成一个随机整数,并通过取模运算将其限制在有效范围内。rand()函数通常返回一个介于0和RAND_MAX(通常为 32767)之间的随机整数。最后,将取模运算的结果加到x1上,以确保随机整数落在指定范围内。
初始化并生成心形点集
create_data函数为动画生成数据。它根据定义的算法计算心形的点并对其进行动画处理。
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) {
// 计算心形的 x 和 y 坐标
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;
}
}
使用循环遍历一系列弧度值(从
0.1到2 * PI),并计算每个点的 x 和 y 坐标。计算两点之间的距离,如果距离大于
average_distance,则将该点存储在origin_points数组中。
为点上色以生成动画
// 在 create_data() 函数内部
index = 0;
for (double size = 0.1, lightness = 1.5; 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 > (double)create_random(0, 100) / 100.0) {
// 随机生成点的颜色、坐标并存储到 Points 数组中
points[index].color = colors[create_random(0, 6)];
points[index].x = size * origin_points[i].x + create_random(-4, 4);
points[index++].y = size * origin_points[i].y + create_random(-4, 4);
}
}
}
有两个嵌套循环。外部循环逐渐增加
size的值,内部循环处理每个quantity个点。根据
success_p的概率,决定是否生成一个点,并将点的颜色和坐标存储到points数组中。
动画生成与更新
// 在 create_data() 函数内部
int points_size = index;
for (int frame = 0; frame < frames; ++frame) {
for (index = 0; index < points_size; ++index) {
// 计算出的点的位置增加并更新坐标
double x = points[index].x, y = points[index].y;
double distance = sqrt(pow(x, 2) + pow(y, 2));
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;
// 使用 XSetForeground 和 XFillArc 绘制点
XSetForeground(display, gc, points[index].color);
XFillArc(display, win, gc, screen_x(points[index].x), screen_y(points[index].y), 2, 2, 0, 360 * 64);
}
for (double size = 17; size < 23; size += 0.3) {
for (index = 0; index < quantity; ++index) {
// 根据条件随机生成点的坐标和颜色,并使用 XSetForeground 和 XFillArc 绘制点
if ((create_random(0, 100) / 100.0 > 0.6 && size >= 20) || (size < 20 && (double)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);
}
XSetForeground(display, gc, colors[create_random(0, 6)]);
XFillArc(display, win, gc, screen_x(x), screen_y(y), 2, 2, 0, 360 * 64);
}
}
}
}
points_size用于获取当前动画帧中的点数,它是根据上一段代码计算得出的。index是之前生成的点数。- 外部循环
for (int frame = 0; frame < frames; ++frame)用于遍历动画的每一帧,frames指定了总帧数。 - 内部循环
for (index = 0; index < points_size; ++index)用于处理当前帧中的每个点。在每一帧中,它执行以下操作:- 首先,计算每个点的新位置。这通过以下公式完成:
double x = points[index].x, y = points[index].y;
double distance = sqrt(pow(x, 2) + pow(y, 2));
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;
这些计算用于更新点的 x 和 y 坐标,以实现动画中点的运动。distance_increase控制着点移动的速度,它随点到其原始位置的距离而变化。- 使用XSetForeground和XFillArc函数绘制点。这将点绘制到屏幕上,XSetForeground用于设置绘图颜色,XFillArc用于绘制填充的点,并且圆的中心坐标通过screen_x和screen_y函数进行转换。4. 内部循环的第二部分for (double size = 17; size < 23; size += 0.3)用于在当前帧中生成额外的点。在这个循环中,每个点被生成、着色并绘制到屏幕上。- 新点的坐标和颜色根据以下条件随机生成:
如果
size >= 20且随机数大于0.6,或者size < 20且随机数大于0.95,则生成一个新点。
- 生成点的 x 和 y 坐标是根据原始点的位置和一些随机偏移量计算得出的。
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);
}
- 最后,使用`XSetForeground`和`XFillArc`函数将生成的点绘制到屏幕上,就像之前的点一样。
创建 X 窗口并进行初始化
此main函数的主要目标是创建一个 X 窗口,初始化数据,然后在一个无限循环中生成并绘制心形动画的每一帧。
int main() {
display = XOpenDisplay(NULL);
int blackColor = BlackPixel(display, DefaultScreen(display));
win = XCreateSimpleWindow(display, RootWindow(display, DefaultScreen(display)), 0, 0, xScreen, yScreen, 0, blackColor, blackColor);
XSelectInput(display, win, StructureNotifyMask);
XMapWindow(display, win);
gc = XCreateGC(display, win, 0, NULL);
}
display:打开与 X 服务器的连接。blackColor:获取显示的黑色像素值。win:创建一个窗口,并指定其属性、位置、大小和边框颜色。XSelectInput:指定窗口的输入事件掩码。XMapWindow:将窗口映射到屏幕上。gc:创建一个图形上下文。
初始化数据并创建点组
// 在 main() 函数内部
while (1) {
XEvent e;
XNextEvent(display, &e);
if (e.type == MapNotify)
break;
}
XFlush(display);
srand(time(NULL));
origin_points = (struct Point*)malloc(quantity * sizeof(struct Point));
points = (struct Point*)malloc(circles * quantity * sizeof(struct Point));
create_data();
while (1)是一个无限循环,用于等待 X 窗口映射完成,这是窗口在屏幕上显示之前的初始化阶段。- 定义一个
XEvent结构体来接收 X 事件。 XNextEvent:等待并获取下一个 X 事件,并将其存储在e变量中。- 使用
if检查事件类型是否为MapNotify,表示窗口已成功映射到屏幕。如果窗口映射完成(即事件类型为MapNotify)。
- 定义一个
XFlush:清空 X 服务器的输出缓冲区,以确保之前的绘图命令生效,不会延迟到后续的动画绘制。srand:初始化随机数生成器。使用当前时间作为随机数生成器的种子,以便在动画中产生随机效果。origin_points:分配内存并创建一个包含quantity个Point结构体的数组,用于存储原始点坐标。points:再次分配内存并创建一个更大的数组来存储动画中的点。circles控制动画中的点数,是一个常量值。- 最后,调用
create_data()函数来初始化数据,生成并设置原始点的坐标,以及初始化动画点的颜色和初始坐标。
动画主循环
此主循环的目的是通过更改frame的值来控制心形图案的状态,从而实现动画效果的扩展和收缩。usleep函数用于控制帧率,以使动画以一定速度播放。
// 在 main() 函数内部
bool extend = true, shrink = false;
for (int frame = 0;;) {
usleep(20000);
XClearWindow(display, win);
if (extend)
frame == 19? (shrink = true, extend = false) : ++frame;
else
frame == 0? (shrink = false, extend = true) : --frame;
}
首先,定义两个布尔变量extend和shrink,并分别初始化为true和false,分别表示心形图案的扩展和收缩状态。
然后,开始一个无限循环,循环中的frame变量用于跟踪当前动画帧的计数。
usleep用于控制动画速度。- 通过调用 X11 函数
XClearWindow清除窗口上的所有内容,以便在下一帧中重新绘制。 - 根据
extend和shrink的值增加或减少frames,以使动画能够扩展和收缩。使用条件运算符? :来设置布尔值。如果
extend为true,检查frame是否等于19。如果是,则动画即将从扩展状态切换到收缩状态,将shrink设置为true,将extend设置为false。否则,增加frame。 如果extend为false,检查frame是否等于0。如果是,则动画即将从收缩状态切换到扩展状态,将shrink设置为false,将extend设置为true。否则,减少frame。
最后,释放资源并退出。
// 在 main() 函数内部
XCloseDisplay(display);
free(origin_points);
free(points);
return 0;
编译并运行项目
- 打开终端并导航到项目目录。
cd ~/project
- 使用以下命令编译代码:
gcc -o dynamic_heart dynamic_heart.c -lX11 -lm
- 运行应用程序:
./dynamic_heart

总结
在这个项目中,你已经学会了如何使用 C 编程语言创建一个引人入胜的动态心形动画。你已经设置了项目,定义了变量,实现了屏幕坐标函数、随机数生成器函数和数据生成函数。该程序利用 X 窗口系统来渲染动画心形。最后,你创建了一个动画循环并关闭了显示,释放了分配的内存。现在,你可以欣赏自己用 C 语言创建的动态心形动画了。



