Crear una animación dinámica de un corazón con C

CBeginner
Practicar Ahora

Introducción

En este proyecto, aprenderás cómo crear una fascinante animación dinámica de un corazón utilizando el lenguaje de programación C. El proyecto utiliza el Sistema de Ventanas X para renderizar visuales animados. Siguiendo las instrucciones paso a paso, configurarás el proyecto, generarás datos y crearás una animación cautivadora que dará vida a un corazón dinámico en tu pantalla.

👀 Vista previa

Dynamic Heart

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo configurar un proyecto de programación en C para crear una animación dinámica de un corazón
  • Cómo utilizar las bibliotecas del Sistema de Ventanas X para crear y gestionar ventanas gráficas
  • Cómo generar puntos aleatorios y animarlos para formar la forma de un corazón
  • Cómo controlar la animación para que se expanda y contraiga, creando un efecto visual cautivador

🏆 Logros

Después de completar este proyecto, podrás:

  • Utilizar las bibliotecas del Sistema de Ventanas X para la programación gráfica en C
  • Generar y manipular puntos aleatorios en C
  • Crear una animación dinámica combinando técnicas de generación de datos y renderizado

Crear los archivos del proyecto

Asegúrate de tener instaladas las bibliotecas necesarias. Necesitarás las bibliotecas de desarrollo de X11. Puedes instalarlas utilizando el siguiente comando:

sudo apt update
sudo apt-get install libx11-dev

Luego, crea un nuevo archivo llamado dynamic_heart.c y ábrelo en tu editor de código preferido.

cd ~/project
touch dynamic_heart.c
✨ Revisar Solución y Practicar

Definir variables necesarias

Primero, necesitamos escribir el código en C. El primer paso es incluir los archivos de encabezado:

#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>

Define la estructura para almacenar la información de un punto, incluyendo su coordenada x, coordenada y y color. Se utiliza para representar puntos en un gráfico.

struct Point {
    double x, y;
    unsigned long color;
};

Luego, define algunas variables globales:

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;

Utiliza variables relacionadas con el Sistema de Ventanas X para crear y gestionar ventanas gráficas y contextos gráficos.

Display *display;
Window win;
GC gc;

display representa la conexión al servidor X, win representa la ventana y gc representa el contexto gráfico, que se utiliza para dibujar los elementos gráficos.

✨ Revisar Solución y Practicar

Implementar funciones de coordenadas de pantalla

double screen_x(double x) {
    x += xScreen / 2;
    return x;
}

double screen_y(double y) {
    y = -y + yScreen / 2;
    return y;
}

El propósito de las funciones screen_x y screen_y es mapear las coordenadas reales a las coordenadas de la pantalla para garantizar que los elementos gráficos de la animación estén dentro del área visible de la pantalla y se muestren en la posición correcta. Para ser más específicos:

  1. screen_x:
  • Esta función toma un valor de coordenada x real como argumento y luego lo ajusta para que se dibuje correctamente en la pantalla. Suma el valor de la coordenada x de entrada a la mitad de xScreen, de modo que estará en el centro horizontal de la pantalla, ya que xScreen representa el ancho de la pantalla, dividido por 2 para encontrar el centro horizontal de la pantalla.
  • La función devuelve el valor de la coordenada x ajustado para dibujar puntos o formas en la pantalla.
  1. screen_y:
  • Esta función es similar a screen_x, pero se ocupa de las coordenadas y. Toma el valor real de la coordenada y como parámetro y lo convierte al sistema de coordenadas de la pantalla. Primero, invierte la coordenada y para que el origen del sistema de coordenadas esté en la esquina superior izquierda de la pantalla, luego la suma a la mitad de yScreen para que la coordenada esté en el centro vertical de la pantalla.
  • La función devuelve el valor de la coordenada y ajustado para que un punto o forma se dibuje correctamente en la pantalla.
✨ Revisar Solución y Practicar

Implementar la función de generador de números aleatorios

int create_random(int x1, int x2) {
    if (x2 > x1) {
        return rand() % (x2 - x1 + 1) + x1;
    }
    return 0;
}

Esta es una función utilizada para generar enteros aleatorios dentro de un rango especificado, y se utiliza para aleatorizar el color y la posición de los puntos generados para aumentar la variedad visual de la animación.

Donde x1 y x2 son los dos enteros pasados como argumentos. La función devuelve un entero aleatorio entre x1 y x2, incluyendo x1 y x2. Si x2 es mayor que x1, se genera un entero aleatorio utilizando la función rand(), que se restringe a un rango válido mediante aritmética modular. La función rand() generalmente devuelve un entero aleatorio entre 0 y RAND_MAX (generalmente 32767). Finalmente, se suma el resultado de la operación modular a x1 para garantizar que el entero aleatorio esté dentro del rango especificado.

✨ Revisar Solución y Practicar

Inicializar y generar conjuntos de puntos en forma de corazón

La función create_data genera los datos para la animación. Calcula los puntos de una forma de corazón y los anima según el algoritmo definido.

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) {
        // Calculate the x and y coordinates of the heart
        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) {
            // Qualifying heart points are stored
            x1 = x2;
            y1 = y2;
            origin_points[index].x = x2;
            origin_points[index++].y = y2;
        }
    }
  • Utiliza un bucle para iterar a través de una serie de valores de radianes (desde 0.1 hasta 2 * PI) y calcular las coordenadas x e y de cada punto.

  • Se calcula la distancia entre dos puntos, y si la distancia es mayor que average_distance, el punto se almacena en la matriz origin_points.

✨ Revisar Solución y Practicar

Colorear puntos para generar animación

// Inside the create_data() function
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) {
            // Randomly generate the color of the points, coordinates, and store them in the Points array
            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);
        }
    }
}
  • Hay dos bucles anidados. El bucle externo aumenta gradualmente el valor de size, y el bucle interno procesa cada punto de quantity.

  • Con una probabilidad dada por success_p, se decide si se debe generar un punto y se almacenan el color y las coordenadas del punto en la matriz points.

✨ Revisar Solución y Practicar

Generación y actualización de animación

// Inside the create_data() function
int points_size = index;

for (int frame = 0; frame < frames; ++frame) {
    for (index = 0; index < points_size; ++index) {
        // The position of the calculated point is increased and the coordinates are updated
        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;
        // Draw points using XSetForeground and 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) {
            // Randomly generate the coordinates and color of the point according to the condition, and draw the point using XSetForeground and 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);
            }
        }
    }
}
  1. points_size se utiliza para obtener el número de puntos en el cuadro actual de la animación, calculado a partir de la sección de código anterior. index es el número de puntos generados previamente.

  2. El bucle externo for (int frame = 0; frame < frames; ++frame) se utiliza para iterar sobre cada cuadro de la animación, y frames especifica cuántos cuadros hay en total.

  3. El bucle interno for (index = 0; index < points_size; ++index) se utiliza para procesar cada punto en el cuadro actual. En cada cuadro, se realiza lo siguiente:

  • Primero, se calcula la nueva posición de cada punto. Esto se hace mediante la siguiente fórmula:
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;

Estos cálculos se utilizan para actualizar las coordenadas x e y del punto para lograr el movimiento del punto en la animación. distance_increase controla la velocidad a la que se mueve el punto, que varía con la distancia desde la posición original del punto.

  • Se dibujan puntos utilizando las funciones XSetForeground y XFillArc. Esto dibuja el punto en la pantalla, XSetForeground para establecer el color de pintura, XFillArc para dibujar un punto relleno, y las coordenadas del centro del círculo se convierten mediante las funciones screen_x y screen_y.
  1. La segunda parte del bucle interno for (double size = 17; size < 23; size += 0.3) se utiliza para generar puntos adicionales en el cuadro actual. En este bucle, se genera, se colorea y se dibuja cada punto en la pantalla.
  • Las coordenadas y los colores de los nuevos puntos se generan aleatoriamente según las siguientes condiciones:

Si size >= 20 y el número aleatorio es mayor que 0.6, o size < 20 y el número aleatorio es mayor que 0.95, se genera un nuevo punto.

  • Las coordenadas x e y de los puntos generados se calculan a partir de la posición del punto original y algunos desplazamientos aleatorios.
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);
}
  • Finalmente, se utilizan las funciones XSetForeground y XFillArc para dibujar el punto generado en la pantalla, al igual que el punto anterior.
✨ Revisar Solución y Practicar

Crear y inicializar una ventana X

El objetivo principal de esta función main es crear una ventana X, inicializar los datos y luego generar y dibujar cada cuadro de la animación en forma de corazón en un bucle infinito.

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: Abre la conexión con el servidor X.
  • blackColor: Obtiene el valor del píxel negro mostrado.
  • win: Crea una ventana y especifica sus propiedades, ubicación, tamaño y color del borde.
  • XSelectInput: Especifica la máscara de eventos de entrada para la ventana.
  • XMapWindow: Mapea la ventana en la pantalla.
  • gc: Crea un Contexto Gráfico (Graphics Context).
✨ Revisar Solución y Practicar

Inicializa los datos y crea grupos de puntos

// Inside the main() function
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();
  1. while (1) es un bucle infinito que espera a que se complete el mapeo de la ventana X, que es la fase de inicialización antes de que la ventana se muestre en la pantalla.
  • Define una estructura XEvent para recibir eventos X.
  • XNextEvent: Espera y obtiene el siguiente evento X y lo almacena en la variable e.
  • Utiliza if para verificar si el tipo de evento es MapNotify, lo que indica que la ventana se ha mapeado correctamente en la pantalla. Si el mapeo de la ventana se ha completado (es decir, el tipo de evento es MapNotify).
  1. XFlush: Vacía el búfer de salida del servidor X para garantizar que los comandos de dibujo anteriores surtan efecto y no se retrasen hasta un dibujo de animación posterior.

  2. srand: Inicializa el generador de números aleatorios. Utiliza la hora actual como semilla para un generador de números aleatorios para generar efectos aleatorios en una animación.

  3. origin_points: Asigna memoria y crea una matriz de quantity estructuras Point que se utilizarán para almacenar las coordenadas de los puntos originales.

  4. points: Nuevamente, asigna memoria y crea una matriz más grande para almacenar los puntos en la animación. circles controla el número de puntos en la animación y es un valor constante.

  5. Finalmente, se llama a la función create_data() para inicializar los datos, generar y establecer las coordenadas de los puntos originales, e inicializar el color y las coordenadas iniciales de los puntos de la animación.

✨ Revisar Solución y Practicar

Bucle principal de animación

El propósito de este bucle principal es controlar el estado del patrón del corazón cambiando el valor de frame para permitir que el efecto de animación se expanda y contraiga. La función usleep se utiliza para controlar la tasa de cuadros (frame rate) para que la animación se reproduzca a una velocidad determinada.

// Inside the main() function
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;
}

Primero, se definen dos variables booleanas, extend y shrink, y se inicializan a true y false, que representan los estados extendido y contraído del patrón del corazón, respectivamente.

Luego, se inicia un bucle infinito, y la variable frame en el bucle se utiliza para realizar un seguimiento del recuento de los cuadros actuales de la animación.

  • usleep se utiliza para controlar la velocidad de la animación.

  • Se borra todo en la ventana llamando a la función X11 XClearWindow para que se pueda volver a dibujar en el siguiente cuadro.

  • Se incrementa o decrementa frame en función de los valores de extend y shrink para permitir que la animación se expanda y contraiga. Se utiliza el operador condicional ? : para establecer un valor booleano.

Si extend es true, se verifica que frame sea igual a 19. Si es así, la animación está a punto de cambiar del estado extendido al estado contraído, se establece shrink en true y extend en false. De lo contrario, se incrementa frame.

Si extend es false, se verifica que frame sea igual a 0. Si es así, la animación está a punto de cambiar del estado contraído al estado extendido, se establece shrink en false y extend en true. De lo contrario, se decrementa frame.

Finalmente, se liberan los recursos y se sale del programa.

// Inside the main() function
XCloseDisplay(display);
free(origin_points);
free(points);
return 0;
✨ Revisar Solución y Practicar

Compilar y ejecutar el proyecto

  1. Abra su terminal y navegue hasta el directorio del proyecto.
cd ~/project
  1. Compile el código utilizando el siguiente comando:
gcc -o dynamic_heart dynamic_heart.c -lX11 -lm
  1. Ejecute la aplicación:
./dynamic_heart
Dynamic Heart
✨ Revisar Solución y Practicar

Resumen

En este proyecto, has aprendido cómo crear una cautivadora animación dinámica de un corazón utilizando el lenguaje de programación C. Has configurado el proyecto, definido variables, implementado funciones de coordenadas de pantalla, funciones de generador de números aleatorios y funciones de generación de datos. El programa utiliza el Sistema de Ventanas X (X Window System) para renderizar el corazón animado. Finalmente, has creado un bucle de animación y cerrado la pantalla, liberando la memoria asignada. Ahora puedes disfrutar de tu propia animación dinámica de un corazón creada en C.