はじめに
私がRustという言語と再び出会ったのは、暮れも押し詰まった頃のことだった。シェアハウスの六畳一間の部屋で、誰かの足音の気配だけを感じながら、画面に向かっていた。シェアハウスの共用キッチンからは時折、誰かの料理する音が漏れ聞こえてくるが、年末だというのに妙に静かだった。
メモリ安全性という言葉に惹かれたわけでも、高パフォーマンスに心を奪われたわけでもない。ただ、この年の瀬に、誰にも見せることのない花火を打ち上げたかっただけなのだ。深夜のターミナル画面に、デジタルの花を咲かせれば、少しは華やかな年越しになるかもしれない。そんな打算的な期待を胸に、私はコードを書き始めた。
年末最後の実装でもバグを出してしまい、『来年もきっと、たくさんの失敗をして恥ずかしいコードを書くことになるだろうな』と考えていました。https://t.co/34hgesMGQu pic.twitter.com/k7f7GyqLoT
— nwiizo (@nwiizo) 2025年1月2日
準備
まずは、以下の外部クレートをインストールします。
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時、ようやく完成したプログラムが描き出す花火の光に、私は密かな充実感を覚えていた。誰にも見せることはないだろうこの花火は、確かに私だけの新年を祝福していたのだから。
来年は少し早めに取り掛かろう。もっとも、来年も誰かと過ごすことになるとは限らないが。これは私の経験則である。知らんけど。
完全なコードは以下の通りです。