じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

Rustで花火アニメーションと新年メッセージを作ろう

はじめに

私がRustという言語と再び出会ったのは、暮れも押し詰まった頃のことだった。シェアハウスの六畳一間の部屋で、誰かの足音の気配だけを感じながら、画面に向かっていた。シェアハウスの共用キッチンからは時折、誰かの料理する音が漏れ聞こえてくるが、年末だというのに妙に静かだった。

メモリ安全性という言葉に惹かれたわけでも、高パフォーマンスに心を奪われたわけでもない。ただ、この年の瀬に、誰にも見せることのない花火を打ち上げたかっただけなのだ。深夜のターミナル画面に、デジタルの花を咲かせれば、少しは華やかな年越しになるかもしれない。そんな打算的な期待を胸に、私はコードを書き始めた。

準備

まずは、以下の外部クレートをインストールします。

  • chrono: 日付と時刻を扱うためのクレート
  • colored: ターミナル出力に色をつけるためのクレート
  • rand: 乱数を生成するためのクレート

Cargo.tomlに以下の行を追加してください。

[dependencies]
rand = "0.8"
colored = "2.0"
chrono = "0.4"

構造体と列挙型の詳細

Fireworkとその周辺の構造体

struct Firework {
    x: f64,           // x座標
    y: f64,           // y座標
    velocity: f64,    // 上昇速度
    particles: Vec<Particle>,  // 爆発後のパーティクル
    exploded: bool,   // 爆発したかどうか
    color: Color,     // 花火の色
    sparkles: Vec<Sparkle>,   // 打ち上げ時の火花
}

struct Sparkle {
    x: f64,           // 火花のx座標
    y: f64,           // 火花のy座標
    lifetime: i32,    // 火花の寿命
}

Firework構造体は花火1発分の情報を管理します。打ち上げ時にはexplodedがfalseで、上昇中の花火を表示。爆発後はexplodedがtrueとなり、particlesに格納された粒子が広がっていきます。sparklesは打ち上げ中の火花を表現するために使用されます。

Particleの詳細

struct Particle {
    x: f64,           // x座標
    y: f64,           // y座標
    vx: f64,          // x方向の速度
    vy: f64,          // y方向の速度
    lifetime: i32,    // パーティクルの寿命
    char: char,       // 表示する文字
    color: Color,     // パーティクルの色
    trail: Vec<(f64, f64)>,  // 軌跡の座標履歴
}

Particleは爆発後の火花を表現します。物理演算で放物線を描くように、速度と重力の影響を受けます。trailは軌跡を表示するために過去の座標を記録しています。

色の実装

#[derive(Clone, Copy)]
enum Color {
    Red, Green, Blue, Yellow, Magenta, Cyan,
    Rainbow,  // 時間とともに色が変化
    Silver,   // 明滅する白
    Gold,     // 明滅する黄色
    Pearl,    // 白と水色で明滅
}

Colorは単色だけでなく、Rainbowのような動的な色変化や、Silver/Gold/Pearlのような明滅効果も実装しています。get_colored_charメソッドで、時間(フレーム数)に応じた色を返します。

アニメーションの仕組み

花火の更新処理

impl Firework {
    fn update(&mut self) {
        if !self.exploded {
            self.y -= self.velocity;  // 上昇
            // 確率で火花を追加
            if rand::thread_rng().gen_bool(0.3) {
                self.sparkles.push(Sparkle {...});
            }
            // 一定の高さで爆発
            if self.y <= rand::thread_rng().gen_range(5.0..15.0) {
                self.explode();
            }
        } else {
            // パーティクルの更新と寿命切れの除去
            for particle in &mut self.particles {
                particle.update();
            }
            self.particles.retain(|p| p.lifetime > 0);
        }
    }
}

花火は打ち上げ時と爆発後で異なる動きをします。打ち上げ中は上昇しながら火花を散らし、一定の高さで爆発。爆発後は多数のパーティクルが放物線を描きながら広がります。

描画処理の工夫

fn draw_frame(fireworks: &Vec<Firework>, frame_count: u32) {
    // 背景に星を表示(10フレームごとに配置を変える)
    if frame_count % 10 == 0 {
        for _ in 0..50 {
            let x = rand::thread_rng().gen_range(0..100);
            let y = rand::thread_rng().gen_range(0..30);
            frame[y][x] = ('·', Some(Color::Silver));
        }
    }
    
    // 各花火の描画
    for firework in fireworks {
        // 打ち上げ火花の描画
        for sparkle in &firework.sparkles {
            let x = sparkle.x as usize;
            let y = sparkle.y as usize;
            frame[y][x] = ('。', Some(Color::Pearl));
        }
        
        if !firework.exploded {
            // 上昇中の花火
            frame[y][x] = ('⁂', Some(firework.color));
        } else {
            // 爆発後のパーティクル
            for particle in &firework.particles {
                // 軌跡の描画
                for (i, (trail_x, trail_y)) in particle.trail.iter().enumerate() {
                    let char = match i {
                        0 => '.',
                        1 => '·',
                        _ => '°',
                    };
                    frame[y][x] = (char, Some(particle.color));
                }
            }
        }
    }
}

描画処理では、まず背景に点滅する星を配置し、その上に花火を重ねていきます。パーティクルの軌跡は徐々に薄くなるように文字を変えています。これにより、より自然な花火の表現を実現しています。

アニメーションとメッセージ表示

最後に、main関数でアニメーションとメッセージ表示を行います。

fn main() {
    let year = Local::now().year(); // 現在の年を取得
    let mut fireworks = Vec::new();
    let mut frame_count = 0;

    loop {
        // 花火を追加
        if frame_count % 15 == 0 && fireworks.len() < 8 {
            fireworks.push(Firework::new(rand::thread_rng().gen_range(10.0..90.0)));
        }

        // 花火を更新して描画
        for firework in &mut fireworks {
            firework.update();
        }
        draw_frame(&fireworks, frame_count);
        fireworks.retain(|f| !f.is_done());

        thread::sleep(Duration::from_millis(40));
        frame_count += 1;

        // 一定時間後にメッセージを表示
        if frame_count > 300 {
            clear_screen();
            display_new_year_message(year);
            break;
        }
    }
}

ループ内で、一定の間隔で新しい花火を追加し、既存の花火を更新して描画しています。300フレーム後には、display_new_year_message関数を呼び出して新年のメッセージを表示します。この関数では、ASCIIアートを使ってメッセージを作成し、coloredクレートで色をつけています。

まとめ

結局、プログラムは年越しに間に合わなかった。シェアハウスの他の住人たちは、それぞれの実家や友人たちの元へと消えていき、廊下は一層静かになっていた。コンパイラの指摘する数々のエラーと向き合ううち、除夜の鐘が鳴り響き、新年は音もなく明けてしまった。

しかし不思議なことに、深夜2時、ようやく完成したプログラムが描き出す花火の光に、私は密かな充実感を覚えていた。誰にも見せることはないだろうこの花火は、確かに私だけの新年を祝福していたのだから。

来年は少し早めに取り掛かろう。もっとも、来年も誰かと過ごすことになるとは限らないが。これは私の経験則である。知らんけど。

完全なコードは以下の通りです。

github.com