Criando uma Animação de Coração Dinâmico com C

CBeginner
Pratique Agora

Introdução

Neste projeto, você aprenderá como criar uma hipnotizante animação de coração dinâmico usando a linguagem de programação C. O projeto utiliza o X Window System para renderizar visuais animados. Seguindo as instruções passo a passo, você configurará o projeto, gerará dados e criará uma animação cativante que dá vida a um coração dinâmico na sua tela.

👀 Pré-visualização

Dynamic Heart

🎯 Tarefas

Neste projeto, você aprenderá:

  • Como configurar um projeto de programação C para criar uma animação de coração dinâmico
  • Como usar bibliotecas do X Window System para criar e gerenciar janelas gráficas
  • Como gerar pontos aleatórios e animá-los para formar um formato de coração
  • Como controlar a animação para expandir e contrair, criando um efeito visual cativante

🏆 Conquistas

Após concluir este projeto, você será capaz de:

  • Usar bibliotecas do X Window System para programação gráfica em C
  • Gerar e manipular pontos aleatórios em C
  • Criar uma animação dinâmica usando uma combinação de geração de dados e técnicas de renderização

Criar os Arquivos do Projeto

Certifique-se de ter as bibliotecas necessárias instaladas. Você precisará das bibliotecas de desenvolvimento X11. Você pode instalá-las usando o seguinte comando:

sudo apt update
sudo apt-get install libx11-dev

Em seguida, crie um novo arquivo chamado dynamic_heart.c e abra-o no seu editor de código preferido.

cd ~/project
touch dynamic_heart.c
✨ Verificar Solução e Praticar

Definir as Variáveis Necessárias

Primeiramente, precisamos escrever o código C. O primeiro passo é incluir os arquivos de cabeçalho:

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

Defina a estrutura para armazenar informações de pontos, incluindo sua coordenada x, coordenada y e cor. Usado para representar pontos em um gráfico.

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

Em seguida, defina algumas variáveis globais:

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;

Use variáveis relacionadas ao X Window System para criar e gerenciar janelas gráficas e contextos gráficos.

Display *display;
Window win;
GC gc;

display representa a conexão com o servidor X, win representa a janela e gc representa o contexto gráfico, que é usado para desenhar os elementos gráficos.

✨ Verificar Solução e Praticar

Implementar Funções de Coordenadas da Tela

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

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

O propósito das funções screen_x e screen_y é mapear as coordenadas reais para as coordenadas da tela, garantindo que os elementos gráficos na animação estejam dentro da área visível da tela e sejam exibidos na posição correta. Para ser mais específico:

  1. screen_x:
  • Esta função recebe um valor de coordenada x real como argumento e, em seguida, o ajusta para que seja desenhado corretamente na tela. Ela adiciona o valor da coordenada x de entrada à metade de xScreen, de modo que fique no centro horizontal da tela, uma vez que xScreen representa a largura da tela, dividida por 2 para encontrar o centro horizontal da tela.
  • A função retorna o valor da coordenada x ajustada para desenhar pontos ou formas na tela.
  1. screen_y:
  • Esta função é semelhante a screen_x, mas lida com as coordenadas y. Ela recebe o valor da coordenada y real como parâmetro e o converte para o sistema de coordenadas da tela. Primeiro, ela inverte a coordenada y para que a origem do sistema de coordenadas esteja no canto superior esquerdo da tela, em seguida, adiciona-a à metade de yScreen para que a coordenada esteja no centro vertical da tela.
  • A função retorna o valor da coordenada y ajustada para que um ponto ou forma seja desenhado corretamente na tela.
✨ Verificar Solução e Praticar

Implementar Função Geradora de Números Aleatórios

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

Esta é uma função usada para gerar inteiros aleatórios dentro de um intervalo especificado, e é utilizada para randomizar a cor e a posição dos pontos gerados, a fim de aumentar a variedade visual da animação.

Onde x1 e x2 são os dois inteiros passados como argumentos. A função retorna um inteiro aleatório entre x1 e x2, incluindo x1 e x2. Se x2 for maior que x1, um inteiro aleatório é gerado usando a função rand(), que é restrita a um intervalo válido usando aritmética modular. A função rand() geralmente retorna um inteiro aleatório entre 0 e RAND_MAX (geralmente 32767). Finalmente, ela adiciona o resultado da operação modular a x1 para garantir que o inteiro aleatório caia dentro do intervalo especificado.

✨ Verificar Solução e Praticar

Inicializar e Gerar Conjuntos de Pontos em Forma de Coração

A função create_data gera dados para a animação. Ela calcula pontos para uma forma de coração e os anima com base no 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;
        }
    }
  • Use um loop para iterar por uma série de valores de radianos (de 0.1 a 2 * PI) e calcular as coordenadas x e y de cada ponto.

  • A distância entre dois pontos é calculada, e se a distância for maior que average_distance, o ponto é armazenado no array origin_points.

✨ Verificar Solução e Praticar

Colorir Pontos para Gerar Animação

// 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);
        }
    }
}
  • Existem dois loops aninhados. O loop externo aumenta gradualmente o valor de size, e o loop interno processa cada ponto quantity.

  • Com a probabilidade de success_p, decide-se se um ponto deve ser gerado e armazena-se a cor e as coordenadas do ponto no array points.

✨ Verificar Solução e Praticar

Geração e Atualização da Animação

// 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 é usado para obter o número de pontos no quadro de animação atual, calculado a partir da seção de código anterior. index é o número de pontos gerados anteriormente.

  2. O loop externo for (int frame = 0; frame < frames; ++frame) é usado para iterar sobre cada quadro da animação, e frames especifica quantos quadros existem no total.

  3. O loop interno for (index = 0; index < points_size; ++index) é usado para processar cada ponto no quadro atual. Em cada quadro, ele faz o seguinte:

  • Primeiro, calcula a nova posição de cada ponto. Isso é feito pela seguinte 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;

Esses cálculos são usados para atualizar as coordenadas x e y do ponto para alcançar o movimento do ponto na animação. distance_increase controla a velocidade com que o ponto se move, que varia com a distância da posição original do ponto.

  • Desenha pontos usando as funções XSetForeground e XFillArc. Isso desenha o ponto na tela, XSetForeground para definir a cor da pintura, XFillArc para desenhar um ponto preenchido, e as coordenadas do centro do círculo são convertidas pelas funções screen_x e screen_y.
  1. A segunda parte do loop interno for (double size = 17; size < 23; size += 0.3) é usada para gerar pontos adicionais no quadro atual. Neste loop, cada ponto é gerado, colorido e desenhado na tela.
  • As coordenadas e cores dos novos pontos são geradas aleatoriamente de acordo com as seguintes condições:

Se size >= 20 e o número aleatório for maior que 0.6, ou size < 20 e o número aleatório for maior que 0.95, um novo ponto é gerado.

  • As coordenadas x e y dos pontos gerados são calculadas a partir da posição do ponto original e alguns deslocamentos aleatórios.
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, use as funções XSetForeground e XFillArc para desenhar o ponto gerado na tela, assim como o ponto anterior.
✨ Verificar Solução e Praticar

Criar Janela X e Inicializar

O objetivo principal desta função main é criar uma janela X, inicializar os dados e, em seguida, gerar e desenhar cada quadro da animação em forma de coração em um loop 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 a conexão com o servidor X.
  • blackColor: Obtém o valor do pixel preto exibido.
  • win: Cria uma janela e especifica suas propriedades, localização, tamanho e cor da borda.
  • XSelectInput: Especifica a máscara de evento de entrada para a janela.
  • XMapWindow: Mapeia a janela para a tela.
  • gc: Cria um Contexto Gráfico (Graphics Context).
✨ Verificar Solução e Praticar

Inicializa Dados e Cria Grupos de Pontos

// 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) é um loop infinito que espera a conclusão do mapeamento da janela X, que é a fase de inicialização antes que a janela seja exibida na tela.
  • Define uma estrutura XEvent para receber eventos X.
  • XNextEvent: Espera e obtém o próximo evento X e o armazena na variável e.
  • Usa if para verificar se o tipo do evento é MapNotify, indicando que a janela foi mapeada com sucesso para a tela. Se o mapeamento da janela estiver completo (ou seja, o tipo de evento é MapNotify).
  1. XFlush: Esvazia o buffer de saída do servidor X para garantir que os comandos de desenho anteriores entrem em vigor e não sejam adiados para um desenho de animação subsequente.

  2. srand: Inicializa o gerador de números aleatórios. Usa o tempo atual como semente para um gerador de números aleatórios para gerar efeitos aleatórios em uma animação.

  3. origin_points: Aloca memória e cria um array de quantity estruturas Point que serão usadas para armazenar as coordenadas originais dos pontos.

  4. points: Novamente, aloca memória e cria um array maior para armazenar os pontos na animação. circles controla o número de pontos na animação e é um valor constante.

  5. Finalmente, a função create_data() é chamada para inicializar os dados, gerar e definir as coordenadas dos pontos originais e inicializar a cor e as coordenadas iniciais dos pontos animados.

✨ Verificar Solução e Praticar

Loop Principal da Animação

O objetivo deste loop principal é controlar o estado do padrão do coração, alterando o valor de frame para permitir que o efeito de animação se expanda e contraia. A função usleep é usada para controlar a taxa de quadros, de modo que a animação seja reproduzida a uma determinada velocidade.

// 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;
}

Primeiro, define-se duas variáveis booleanas, extend e shrink, e inicializa-se com true e false, representando os estados expandido e contraído do padrão do coração, respectivamente.

Em seguida, um loop infinito é iniciado, e a variável frame no loop é usada para rastrear a contagem dos quadros de animação atuais.

  • usleep é usado para controlar a velocidade da animação.

  • Limpa tudo na janela chamando a função X11 XClearWindow para que possa ser redesenhado no próximo quadro.

  • Adiciona ou diminui frames com base nos valores extend e shrink para permitir que a animação se expanda e contraia. O operador condicional ? : é usado para definir um valor booleano.

Se extend for true, verifica-se se frame é igual a 19. Se for, a animação está prestes a mudar do estado expandido para o estado contraído, define-se shrink como true e extend como false. Caso contrário, incrementa-se frame.

Se extend for false, verifica-se se frame é igual a 0. Se for, a animação está prestes a mudar do estado contraído para o estado expandido, define-se shrink como false e extend como true. Caso contrário, frame é decrementado.

Finalmente, libera-se o recurso e sai-se.

// Inside the main() function
XCloseDisplay(display);
free(origin_points);
free(points);
return 0;
✨ Verificar Solução e Praticar

Compilar e Executar o Projeto

  1. Abra seu terminal e navegue até o diretório do projeto.
cd ~/project
  1. Compile o código usando o seguinte comando:
gcc -o dynamic_heart dynamic_heart.c -lX11 -lm
  1. Execute a aplicação:
./dynamic_heart
Dynamic Heart
✨ Verificar Solução e Praticar

Resumo

Neste projeto, você aprendeu como criar uma animação de coração dinâmica cativante usando a linguagem de programação C. Você configurou o projeto, definiu variáveis, implementou funções de coordenadas de tela, funções de gerador de números aleatórios e funções de geração de dados. O programa utiliza o X Window System para renderizar o coração animado. Finalmente, você criou um loop de animação e fechou a exibição, liberando a memória alocada. Agora você pode desfrutar de sua própria animação de coração dinâmica criada em C.