C 言語で動的なハートアニメーションを作成する

CBeginner
オンラインで実践に進む

はじめに

このプロジェクトでは、C プログラミング言語を使用して魅力的な動的なハートアニメーションを作成する方法を学びます。このプロジェクトでは、X Window System を利用してアニメーションのビジュアルをレンダリングします。ステップバイステップの指示に従うことで、プロジェクトをセットアップし、データを生成し、画面上に動的なハートを生き生きと表現する魅力的なアニメーションを作成することができます。

👀 プレビュー

Dynamic Heart

🎯 タスク

このプロジェクトでは、以下のことを学びます。

  • 動的なハートアニメーションを作成するための C プログラミングプロジェクトをセットアップする方法
  • X Window System ライブラリを使用してグラフィカルなウィンドウを作成および管理する方法
  • ランダムな点を生成し、それらをアニメーション化してハートの形を形成する方法
  • アニメーションを制御して拡大と縮小を行い、魅力的な視覚効果を生み出す方法

🏆 達成目標

このプロジェクトを完了した後、以下のことができるようになります。

  • C でのグラフィカルなプログラミングに X Window System ライブラリを使用する
  • 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 System 関連の変数を使用して、グラフィカルなウィンドウとグラフィックスコンテキストを作成および管理します。

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_xscreen_y 関数の目的は、実際の座標を画面座標にマッピングし、アニメーション内のグラフィック要素が画面の可視領域内に収まり、正しい位置に表示されるようにすることです。具体的には以下の通りです。

  1. screen_x
  • この関数は実際の x 座標値を引数として受け取り、それを調整して画面上に正しく描画されるようにします。入力された x 座標値に xScreen の半分を加えることで、画面の水平方向の中央に配置されます。xScreen は画面の幅を表し、2 で割ることで画面の水平方向の中央を求めます。
  • この関数は、画面上に点や形状を描画するために調整された x 座標値を返します。
  1. 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;
}

これは、指定された範囲内で乱数の整数を生成するための関数です。生成される点の色と位置をランダム化し、アニメーションの視覚的なバリエーションを増やすために使用されます。

ここで、x1x2 は引数として渡される 2 つの整数です。この関数は、x1x2 の間(x1x2 を含む)の乱数の整数を返します。x2x1 より大きい場合、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) {
        // 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;
        }
    }
  • ループを使用して、一連のラジアン値(0.1 から 2 * PI)を反復処理し、各点の x 座標と y 座標を計算します。

  • 2 点間の距離を計算し、その距離が average_distance より大きい場合、その点を origin_points 配列に格納します。

✨ 解答を確認して練習

点を着色してアニメーションを生成する

// 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);
        }
    }
}
  • 2 つの入れ子になったループがあります。外側のループは size の値を徐々に増やし、内側のループは各 quantity の点を処理します。

  • success_p の確率で、点を生成するかどうかを決定し、点の色と座標を points 配列に格納します。

✨ 解答を確認して練習

アニメーションの生成と更新

// 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 は、現在のアニメーションフレームの点の数を取得するために使用され、前のコードセクションから計算されます。index は、以前に生成された点の数です。

  2. 外側のループ for (int frame = 0; frame < frames; ++frame) は、アニメーションの各フレームを反復処理するために使用され、frames は総フレーム数を指定します。

  3. 内側のループ 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 は、点が動く速度を制御し、点の元の位置からの距離に応じて変化します。

  • XSetForegroundXFillArc 関数を使用して点を描画します。これにより、点が画面上に描画されます。XSetForeground は塗りつぶし色を設定するために、XFillArc は塗りつぶされた点を描画するために使用され、円の中心の座標は screen_xscreen_y 関数によって変換されます。
  1. 内側のループの 2 番目の部分 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);
}
  • 最後に、前の点と同じように、XSetForegroundXFillArc 関数を使用して、生成された点を画面上に描画します。
✨ 解答を確認して練習

X Window を作成して初期化する

この 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: グラフィックスコンテキスト (Graphics Context) を作成します。
✨ 解答を確認して練習

データを初期化して点群を作成する

// 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) は無限ループで、X ウィンドウのマッピングが完了するのを待ちます。これは、ウィンドウが画面に表示される前の初期化フェーズです。
  • XEvent 構造体を定義して、X イベントを受け取ります。
  • XNextEvent: 次の X イベントを待機して取得し、e 変数に格納します。
  • if を使用して、イベントのタイプが MapNotify であるかどうかを確認します。これは、ウィンドウが画面に正常にマッピングされたことを示します。ウィンドウのマッピングが完了した場合(つまり、イベントタイプが MapNotify である場合)。
  1. XFlush: X サーバーの出力バッファを空にして、以前の描画コマンドが有効になり、後続のアニメーション描画に遅延しないようにします。

  2. srand: 乱数生成器を初期化します。現在の時刻を乱数生成器のシードとして使用し、アニメーションにランダムな効果を生み出します。

  3. origin_points: メモリを割り当て、quantity 個の Point 構造体の配列を作成します。これは、元の点の座標を格納するために使用されます。

  4. points: 再度メモリを割り当て、より大きな配列を作成して、アニメーション内の点を格納します。circles はアニメーション内の点の数を制御し、定数値です。

  5. 最後に、create_data() 関数を呼び出してデータを初期化し、元の点の座標を生成して設定し、アニメーションの点の色と初期座標を初期化します。

✨ 解答を確認して練習

アニメーションのメインループ

このメインループの目的は、frame の値を変更することでハートパターンの状態を制御し、アニメーション効果を拡大と縮小させることです。usleep 関数を使用してフレームレートを制御し、アニメーションが一定の速度で再生されるようにします。

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

まず、2 つのブール型変数 extendshrink を定義し、それぞれ truefalse に初期化します。これらは、ハートパターンの拡大状態と縮小状態を表します。

次に、無限ループを開始し、ループ内の frame 変数を使用して現在のアニメーションフレームのカウントを追跡します。

  • usleep はアニメーションの速度を制御するために使用されます。

  • X11 関数 XClearWindow を呼び出してウィンドウ上のすべてをクリアし、次のフレームで再描画できるようにします。

  • extendshrink の値に基づいて frame を増減させ、アニメーションを拡大または縮小させます。条件演算子 ? : を使用してブール値を設定します。

extendtrue の場合、frame19 と等しいかどうかを確認します。もしそうであれば、アニメーションが拡大状態から縮小状態に切り替わろうとしているため、shrinktrue に、extendfalse に設定します。そうでなければ、frame をインクリメントします。

extendfalse の場合、frame0 と等しいかどうかを確認します。もしそうであれば、アニメーションが縮小状態から拡大状態に切り替わろうとしているため、shrinkfalse に、extendtrue に設定します。そうでなければ、frame をデクリメントします。

最後に、リソースを解放して終了します。

// Inside the main() function
XCloseDisplay(display);
free(origin_points);
free(points);
return 0;
✨ 解答を確認して練習

プロジェクトをコンパイルして実行する

  1. ターミナルを開き、プロジェクトディレクトリに移動します。
cd ~/project
  1. 次のコマンドを使用してコードをコンパイルします。
gcc -o dynamic_heart dynamic_heart.c -lX11 -lm
  1. アプリケーションを実行します。
./dynamic_heart
Dynamic Heart
✨ 解答を確認して練習

まとめ

このプロジェクトでは、C プログラミング言語を使用して魅力的な動的なハートアニメーションを作成する方法を学びました。プロジェクトのセットアップ、変数の定義、画面座標関数、乱数生成関数、およびデータ生成関数の実装を行いました。このプログラムは X Window System を利用してアニメーション化されたハートをレンダリングします。最後に、アニメーションループを作成し、ディスプレイを閉じ、割り当てられたメモリを解放しました。これで、C 言語で作成した独自の動的なハートアニメーションを楽しむことができます。