Repository: Wayoung7/firework-rs Branch: master Commit: dc9213df5b7e Files: 20 Total size: 80.2 KB Directory structure: gitextract_j471o5sk/ ├── .github/ │ └── workflows/ │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples/ │ ├── fountain.rs │ ├── heart.rs │ └── vortex.rs └── src/ ├── bin/ │ └── firework/ │ ├── args.rs │ ├── gen.rs │ └── main.rs ├── config.rs ├── demo.rs ├── fireworks.rs ├── lib.rs ├── particle.rs ├── term.rs └── utils.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: pull_request: push: branches: - master - v0.3.0 schedule: - cron: '00 01 * * *' jobs: check: name: Check runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: Swatinem/rust-cache@v1 - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check test: name: Test Suite strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] rust: [stable] runs-on: ${{ matrix.os }} steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true - uses: Swatinem/rust-cache@v1 - name: Run cargo test uses: actions-rs/cargo@v1 with: command: test lints: name: Lints runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 with: submodules: true - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true components: rustfmt, clippy - uses: Swatinem/rust-cache@v1 - name: Run cargo fmt uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check # - name: Run cargo clippy # uses: actions-rs/cargo@v1 # with: # command: clippy # args: -- -D warnings ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' env: BIN_NAME: firework PROJECT_NAME: firework-rs REPO_NAME: Wayoung7/firework-rs jobs: dist: name: Dist runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: build: [x86_64-linux, aarch64-linux, x86_64-macos, x86_64-windows] include: - build: x86_64-linux os: ubuntu-20.04 rust: stable target: x86_64-unknown-linux-gnu cross: false - build: aarch64-linux os: ubuntu-20.04 rust: stable target: aarch64-unknown-linux-gnu cross: true - build: x86_64-macos os: macos-latest rust: stable target: x86_64-apple-darwin cross: false - build: x86_64-windows os: windows-2019 rust: stable target: x86_64-pc-windows-msvc cross: false steps: - name: Checkout sources uses: actions/checkout@v2 with: submodules: true - name: Install ${{ matrix.rust }} toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} override: true - name: Run cargo test uses: actions-rs/cargo@v1 with: use-cross: ${{ matrix.cross }} command: test args: --release --locked --target ${{ matrix.target }} - name: Build release binary uses: actions-rs/cargo@v1 with: use-cross: ${{ matrix.cross }} command: build args: --release --locked --target ${{ matrix.target }} - name: Strip release binary (linux and macos) if: matrix.build == 'x86_64-linux' || matrix.build == 'x86_64-macos' run: strip "target/${{ matrix.target }}/release/$BIN_NAME" - name: Strip release binary (arm) if: matrix.build == 'aarch64-linux' run: | docker run --rm -v \ "$PWD/target:/target:Z" \ rustembedded/cross:${{ matrix.target }} \ aarch64-linux-gnu-strip \ /target/${{ matrix.target }}/release/$BIN_NAME - name: Build archive shell: bash run: | mkdir dist if [ "${{ matrix.os }}" = "windows-2019" ]; then cp "target/${{ matrix.target }}/release/$BIN_NAME.exe" "dist/" else cp "target/${{ matrix.target }}/release/$BIN_NAME" "dist/" fi - uses: actions/upload-artifact@v2.2.4 with: name: bins-${{ matrix.build }} path: dist publish: name: Publish needs: [dist] runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 with: submodules: false - uses: actions/download-artifact@v2 - run: ls -al bins-* - name: Calculate tag name run: | name=dev if [[ $GITHUB_REF == refs/tags/v* ]]; then name=${GITHUB_REF:10} fi echo ::set-output name=val::$name echo TAG=$name >> $GITHUB_ENV id: tagname - name: Build archive shell: bash run: | set -ex rm -rf tmp mkdir tmp mkdir dist for dir in bins-* ; do platform=${dir#"bins-"} unset exe if [[ $platform =~ "windows" ]]; then exe=".exe" fi pkgname=$PROJECT_NAME-$TAG-$platform mkdir tmp/$pkgname # cp LICENSE README.md tmp/$pkgname mv bins-$platform/$BIN_NAME$exe tmp/$pkgname chmod +x tmp/$pkgname/$BIN_NAME$exe if [ "$exe" = "" ]; then tar cJf dist/$pkgname.tar.xz -C tmp $pkgname else (cd tmp && 7z a -r ../dist/$pkgname.zip $pkgname) fi done - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: dist/* file_glob: true tag: ${{ steps.tagname.outputs.val }} overwrite: true - name: Extract version id: extract-version run: | printf "::set-output name=%s::%s\n" tag-name "${GITHUB_REF#refs/tags/}" ================================================ FILE: .gitignore ================================================ /target .vscode ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. ## [0.3.1](https://github.com/Wayoung7/firework-rs/releases/tag/v0.3.1) - 2024-04-30 ### Changed - Performance enhanced by using `VecDeque` to represent trail and by using macro initialization of `Vec` ## [0.3.0](https://github.com/Wayoung7/firework-rs/releases/tag/v0.3.0) - 2024-04-12 ### Added - CJK(also UTF-8) characters support (it takes twice of space as normal ascii characters) ## [0.2.0](https://github.com/Wayoung7/firework-rs/releases/tag/v0.2.0) - 2024-03-22 ### Added - Firework demo which generates fireworks infinitely and randomly - Command line argument of changing frame rate - New field in `FireworkManager` to control the installation of `Firework`s ### Changed - Implementation of filtering dead `Particle`s ================================================ FILE: Cargo.toml ================================================ [package] name = "firework-rs" version = "0.3.1" authors = ["Wayoung7 "] edition = "2021" description = "A cross-platform ascii-art firework simulator in terminal" readme = "README.md" repository = "https://github.com/Wayoung7/firework-rs" license = "MIT" keywords = ["ascii", "fireworks", "terminal", "cli", "simulation"] categories = ["command-line-utilities"] exclude = ["gif/*"] [dependencies] clap = { version = "4.5.2", features = ["derive"] } crossterm = "0.27.0" glam = "0.25.0" rand = "0.8.5" rand_distr = "0.4.3" [[bin]] name = "firework" path = "src/bin/firework/main.rs" [lib] name = "firework_rs" path = "src/lib.rs" crate-type = ["lib"] bench = false [[example]] name = "fountain" [[example]] name = "vortex" [[example]] name = "heart" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Wayoung7 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================


gif

Firework-rs

crates.io License

Firework-rs is a cross-platform ascii-art firework simulator in terminal. Run the binary or use the library to create your own firework, and just enjoy the beautiful fireworks in your terminal! ## Features - Colorful ASCII art firework - Smooth animation - Customizable fireworks - Simple particle system letting you make fireworks but not only fireworks ## Try Out a Demo Install [rust](https://www.rust-lang.org/tools/install) if you havn't. Then, simply run the following commands: ``` git clone https://github.com/Wayoung7/firework-rs.git cd firework-rs cargo run --release -- -d 0 ``` or to install globally on your computer: ``` cargo install firework-rs firework -d 0 ``` The binary now has **5 demos**, from **0** to **4**. ## Exit To exit the program, simply press `ESC` ## Command Line Arguments ``` USAGE: firework [OPTIONS] --demo Options: -d, --demo Select which demo to run. (optional) If this is not specified, automatically run the infinite random firework demo -l, --looping Set whether the fireworks show will loop infinitely -g, --gradient Set whether the fireworks will have color gradient If this is enabled, it is recommanded that your terminal is non-transparent and has black bg color to get better visual effects --fps Set frame per second If this is not specified, the default fps is 12 --cjk Set whether to enable cjk character If enabled, each character will take up two Latin character space -h, --help Print help (see a summary with '-h') -V, --version Print version ``` ### Example Commands If you have installed the binary: Infinite firework show with gradient enabled: ``` firework -g ``` Demo 1 with looping and gradient enabled: ``` firework -l -g -d 1 ``` If you have not installed the binary: First `cd` into the project root directory, and then run: ``` cargo run --release -- -g ``` ``` cargo run --release -- -l -g -d 1 ``` ## Use the Library This package not only has a demo binary for you to enjoy terminal fireworks, but also provides you with a simple library **firework_rs** to play with your own fireworks. To add this crate to your rust project, run: ``` cargo add firework_rs ``` in your project root directory. To make a firework, you can simply use the following structure: ``` fn main() -> Result<()> { // Terminal stuff, no need to change let mut stdout = stdout(); let (_width, _height) = terminal::size()?; let mut is_running = true; terminal::enable_raw_mode()?; execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?; let mut time = SystemTime::now(); let mut term = Terminal::default(); // Init and add fireworks let mut fm = FireworkManager::default().add_firework(gen()); // Main loop, no need to change while is_running { if event::poll(Duration::ZERO)? { match event::read()? { event::Event::Key(e) => { if e.code == KeyCode::Esc { is_running = false; } } event::Event::Resize(_, _) => { fm.reset(); term.reinit(); } _ => {} }; } let delta_time = SystemTime::now().duration_since(time).unwrap(); fm.update(time, delta_time); time = SystemTime::now(); term.render(&fm); term.print(&mut stdout); if delta_time < Duration::from_secs_f32(0.05) { let rem = Duration::from_secs_f32(0.05) - delta_time; sleep(rem); } } execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?; terminal::disable_raw_mode()?; Ok(()) } // Your actuall firework design goes here, see docs for more information fn gen() -> Firework { let colors = vec![ ... ]; let particles = ... let config = ... Firework { ... } } ``` ### Examples The package provide several examples under `examples/` showing some features of the library, and give you some inspiration. To run examples, `cd` into the this project directory, and simply type: ``` cargo run --example ``` **Example-name** contains: fountain

gif

vortex

gif

heart

gif

## Compatibility ### Operating System This program can be run on Windows / Mac OS / Linux. ### Terminal This crate uses [crossterm](https://github.com/crossterm-rs/crossterm) as backend. Terminals that crossterm supports will also be supported by this crate. This crate supports all UNIX terminals and Windows terminals down to Windows 7. however, not all of the terminals have been tested and has good viusal quality. It is recommanded to use terminal that has GPU rendering acceleration, like [Kitty](https://github.com/kovidgoyal/kitty) and [Alacritty](https://github.com/alacritty/alacritty). Make sure your terminal does not have extra color theme or adjustment. If you enable gradient in the program, make sure the terminal window is **non-transparent** and has **black background**. ## Help Feel free to open an issue or contact me if you find any bugs. ================================================ FILE: examples/fountain.rs ================================================ use std::{ f32::consts::PI, io::{stdout, Result}, thread::sleep, time::{Duration, SystemTime}, }; use crossterm::{ cursor, event::{self, KeyCode}, execute, terminal, }; use firework_rs::{ config::Config, fireworks::{ExplosionForm, Firework, FireworkConfig, FireworkManager}, particle::ParticleConfig, term::Terminal, utils::gen_points_fan, }; use glam::Vec2; use rand::{seq::IteratorRandom, thread_rng, Rng}; fn main() -> Result<()> { let mut stdout = stdout(); let (_width, _height) = terminal::size()?; let mut is_running = true; let cfg = Config::default(); terminal::enable_raw_mode()?; execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?; let mut time = SystemTime::now(); let mut term = Terminal::default(); let mut fm = FireworkManager::default().with_firework(gen_fountain_firework(Vec2::new( _width as f32 / 4., _height as f32 / 2. + 13., ))); while is_running { if event::poll(Duration::ZERO)? { match event::read()? { event::Event::Key(e) => { if e.code == KeyCode::Esc { is_running = false; } } event::Event::Resize(_, _) => { fm.reset(); term.reinit(&cfg); } _ => {} }; } let delta_time = SystemTime::now().duration_since(time).unwrap(); fm.update(time, delta_time); time = SystemTime::now(); term.render(&fm, &cfg); term.print(&mut stdout, &cfg); if delta_time < Duration::from_secs_f32(0.05) { let rem = Duration::from_secs_f32(0.05) - delta_time; sleep(rem); } } execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?; terminal::disable_raw_mode()?; Ok(()) } fn gen_fountain_firework(center: Vec2) -> Firework { let colors = vec![(226, 196, 136), (255, 245, 253), (208, 58, 99)]; let mut particles = Vec::new(); for v in gen_points_fan( 300., 45, 5 as f32 / 12 as f32 * PI, 7 as f32 / 12 as f32 * PI, ) .iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(28..38), Duration::from_secs_f32(thread_rng().gen_range(2.5..3.8)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_ar_scale(0.15) .with_gravity_scale(0.5) .with_gradient_scale(gradient); config.set_enable_gradient(true); Firework { init_time: SystemTime::now(), spawn_after: Duration::ZERO, center, particles, config, form: ExplosionForm::Sustained { lasts: Duration::from_secs(5), time_interval: Duration::from_secs_f32(0.08), timer: Duration::ZERO, }, ..Default::default() } } fn gradient(x: f32) -> f32 { if x < 0.8125 { -0.4 * x + 1.1 } else { -2. * x + 2.4 } } ================================================ FILE: examples/heart.rs ================================================ use std::{ f32::consts::PI, io::{stdout, Result}, thread::sleep, time::{Duration, SystemTime}, }; use crossterm::{ cursor, event::{self, KeyCode}, execute, terminal, }; use firework_rs::{ config::Config, fireworks::{ExplosionForm, Firework, FireworkConfig, FireworkManager}, particle::ParticleConfig, term::Terminal, utils::gen_points_fan, }; use glam::Vec2; use rand::{seq::IteratorRandom, thread_rng, Rng}; fn main() -> Result<()> { let mut stdout = stdout(); let (_width, _height) = terminal::size()?; let mut is_running = true; let cfg = Config::default(); terminal::enable_raw_mode()?; execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?; let mut time = SystemTime::now(); let mut term = Terminal::default(); let mut fm = FireworkManager::default().with_firework(gen_heart_firework(Vec2::new( _width as f32 / 4., _height as f32 / 2., ))); while is_running { if event::poll(Duration::ZERO)? { match event::read()? { event::Event::Key(e) => { if e.code == KeyCode::Esc { is_running = false; } } event::Event::Resize(_, _) => { fm.reset(); term.reinit(&cfg); } _ => {} }; } let delta_time = SystemTime::now().duration_since(time).unwrap(); fm.update(time, delta_time); time = SystemTime::now(); term.render(&fm, &cfg); term.print(&mut stdout, &cfg); if delta_time < Duration::from_secs_f32(0.05) { let rem = Duration::from_secs_f32(0.05) - delta_time; sleep(rem); } } execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?; terminal::disable_raw_mode()?; Ok(()) } fn gen_heart_firework(center: Vec2) -> Firework { let colors = vec![ (233, 232, 237), (254, 142, 130), (200, 27, 72), (86, 18, 31), ]; let mut particles = Vec::new(); let trail_length = thread_rng().gen_range(100..105); let life_time = Duration::from_secs_f32(thread_rng().gen_range(3.0..3.2)); let init_pos = center - Vec2::NEG_Y * 15.; for v in gen_points_fan(300., 45, 0.2 * PI, 0.3 * PI).iter() { particles.push(ParticleConfig::new( init_pos, *v, trail_length, life_time, *colors.iter().choose(&mut thread_rng()).unwrap(), )); } for v in gen_points_fan(300., 45, 0.7 * PI, 0.8 * PI).iter() { particles.push(ParticleConfig::new( init_pos, *v, trail_length, life_time, *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_ar_scale(0.1) .with_gravity_scale(0.1) .with_gradient_scale(gradient) .with_additional_force(move |particle| (center - particle.pos) * 2.); config.set_enable_gradient(true); Firework { init_time: SystemTime::now(), spawn_after: Duration::ZERO, center, particles, config, form: ExplosionForm::Instant { used: false }, ..Default::default() } } fn gradient(x: f32) -> f32 { if x < 0.8125 { -0.4 * x + 1.1 } else { -2. * x + 2.2 } } ================================================ FILE: examples/vortex.rs ================================================ use std::{ io::{stdout, Result}, thread::sleep, time::{Duration, SystemTime}, }; use crossterm::{ cursor, event::{self, KeyCode}, execute, terminal, }; use firework_rs::{ config::Config, fireworks::{ExplosionForm, Firework, FireworkConfig, FireworkManager}, particle::ParticleConfig, term::Terminal, utils::gen_points_circle, }; use glam::Vec2; use rand::{seq::IteratorRandom, thread_rng, Rng}; fn main() -> Result<()> { let mut stdout = stdout(); let (_width, _height) = terminal::size()?; let mut is_running = true; let cfg = Config::default(); terminal::enable_raw_mode()?; execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?; let mut time = SystemTime::now(); let mut term = Terminal::default(); let mut fm = FireworkManager::default().with_firework(gen_vortex_firework(Vec2::new( _width as f32 / 4., _height as f32 / 2., ))); while is_running { if event::poll(Duration::ZERO)? { match event::read()? { event::Event::Key(e) => { if e.code == KeyCode::Esc { is_running = false; } } event::Event::Resize(_, _) => { fm.reset(); term.reinit(&cfg); } _ => {} }; } let delta_time = SystemTime::now().duration_since(time).unwrap(); fm.update(time, delta_time); time = SystemTime::now(); term.render(&fm, &cfg); term.print(&mut stdout, &cfg); if delta_time < Duration::from_secs_f32(0.05) { let rem = Duration::from_secs_f32(0.05) - delta_time; sleep(rem); } } execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?; terminal::disable_raw_mode()?; Ok(()) } fn gen_vortex_firework(center: Vec2) -> Firework { let colors = vec![ (6, 55, 63), (24, 90, 96), (47, 123, 119), (92, 174, 166), (200, 255, 255), ]; let mut particles = Vec::new(); for p in gen_points_circle(30, 45).iter() { particles.push(ParticleConfig::new( center + *p, Vec2::new((*p).y, -(*p).x).normalize() * 15., thread_rng().gen_range(28..40), Duration::from_secs_f32(thread_rng().gen_range(4.5..7.0)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_ar_scale(0.05) .with_gravity_scale(0.) .with_gradient_scale(gradient) .with_additional_force(move |particle| { (center - particle.pos).normalize() * (1. / center.distance(particle.pos)) * 150. }); config.set_enable_gradient(true); Firework { init_time: SystemTime::now(), spawn_after: Duration::ZERO, center, particles, config, form: ExplosionForm::Sustained { lasts: Duration::from_secs(10), time_interval: Duration::from_secs_f32(0.01), timer: Duration::ZERO, }, ..Default::default() } } fn gradient(x: f32) -> f32 { if x < 0.8125 { -0.4 * x + 1.1 } else { -2. * x + 2.2 } } ================================================ FILE: src/bin/firework/args.rs ================================================ use clap::Parser; /// Used to receive command line arguments #[derive(Parser)] #[command(version, about, long_about = None)] pub struct Cli { /// Select which demo to run. (optional) /// /// If this is not specified, automatically run the infinite random firework demo #[arg(short, long, value_name = "DEMO-NUMBER")] pub demo: Option, /// Set whether the fireworks show will loop infinitely #[arg(short, long)] pub looping: bool, /// Set whether the fireworks will have color gradient /// /// If this is enabled, it is recommanded that your terminal is non-transparent and has black bg color to get better visual effects #[arg(short, long)] pub gradient: bool, /// Set frame per second /// /// If this is not specified, the default fps is 12 #[arg(long, value_name = "FRAME-RATE")] pub fps: Option, /// Set whether to enable cjk character /// /// If enabled, each character will take up two Latin character space #[arg(long)] pub cjk: bool, } ================================================ FILE: src/bin/firework/gen.rs ================================================ use std::time::Duration; use firework_rs::{config::Config, demo::demo_firework_0, fireworks::FireworkManager}; use glam::Vec2; use rand::{seq::IteratorRandom, thread_rng, Rng}; pub fn dyn_gen( fm: &mut FireworkManager, width: u16, height: u16, enable_gradient: bool, cfg: &Config, ) { let colors = [ vec![ (255, 102, 75), (144, 56, 67), (255, 225, 124), (206, 32, 41), ], vec![ (235, 39, 155), (250, 216, 68), (242, 52, 72), (63, 52, 200), (255, 139, 57), ], vec![ (152, 186, 227), (89, 129, 177), (54, 84, 117), (240, 244, 254), ], vec![ (34, 87, 122), (56, 163, 165), (87, 204, 153), (128, 237, 153), (199, 249, 204), ], vec![ (205, 180, 219), (255, 200, 221), (255, 175, 204), (189, 224, 254), (162, 210, 255), ], vec![ (79, 0, 11), (114, 0, 38), (206, 66, 87), (255, 127, 81), (255, 155, 84), ], vec![(0, 29, 61), (0, 53, 102), (255, 195, 0), (255, 214, 10)], vec![ (61, 52, 139), (118, 120, 237), (247, 184, 1), (241, 135, 1), (243, 91, 4), ], ]; let limit = if cfg.enable_cjk { (width as usize * height as usize) / 1800 + 3 } else { (width as usize * height as usize) / 1300 + 3 }; if fm.fireworks.len() < limit { let x: isize = thread_rng().gen_range(-3..(width as isize + 3)); let y: isize = thread_rng().gen_range(-1..(height as isize + 1)); fm.add_firework(demo_firework_0( Vec2::new(x as f32, y as f32), Duration::from_secs_f32(thread_rng().gen_range(0.0..2.0)), enable_gradient, colors.iter().choose(&mut thread_rng()).unwrap().to_owned(), cfg, )); } } ================================================ FILE: src/bin/firework/main.rs ================================================ //! With the `firework` binary, you can run some pre-designed fireworks with command line arguments mod args; mod gen; use std::{ io::{stdout, Error, Result}, thread::sleep, time::{Duration, SystemTime}, }; use args::Cli; use clap::Parser; use crossterm::{ cursor, event::{self, KeyCode}, execute, terminal, }; use firework_rs::term::Terminal; use firework_rs::{config::Config, fireworks::FireworkManager}; use firework_rs::{ demo::{ demo_firework_2, demo_firework_comb_0, demo_firework_comb_1, demo_firework_comb_2, demo_firework_comb_3, }, fireworks::FireworkInstallForm, }; use gen::dyn_gen; use glam::Vec2; fn main() -> Result<()> { let mut cfg = Config::default(); let mut fps: u8 = 20; let mut is_running = true; let cli = Cli::parse(); if cli.cjk { cfg = Config { enable_cjk: true }; } if let Some(f) = cli.fps { if !(5..=30).contains(&f) { return Err(Error::new( std::io::ErrorKind::Other, "Invalid fps value! Valid fps range: 5~30", )); } else { fps = f; } } let (mut _width, mut _height) = terminal::size()?; let mut fm = match cli.demo { Some(0) => FireworkManager::default().with_fireworks(demo_firework_comb_0( Vec2::new(_width as f32 / 4., _height as f32 / 2.), Duration::from_secs_f32(0.7), cli.gradient, )), Some(1) => FireworkManager::default().with_fireworks(demo_firework_comb_2( Vec2::new(_width as f32 / 4., _height as f32 / 2.), Duration::from_secs_f32(0.7), cli.gradient, )), Some(2) => FireworkManager::default().with_fireworks(demo_firework_comb_3( Vec2::new(_width as f32 / 4., _height as f32 / 2.), Duration::from_secs_f32(0.7), cli.gradient, )), Some(3) => FireworkManager::default().with_fireworks(demo_firework_comb_1( Vec2::new(_width as f32 / 4., 66.), Duration::from_secs_f32(0.2), cli.gradient, )), Some(4) => FireworkManager::default().with_firework(demo_firework_2( Vec2::new(_width as f32 / 4., _height as f32 / 2.), Duration::from_secs_f32(0.7), cli.gradient, )), None => FireworkManager::default().enable_dyn_install(), _ => { return Err(Error::new( std::io::ErrorKind::Other, "Invalid demo number! Demo number should be: 0~4", )); } }; fm.set_enable_loop(cli.looping); let mut stdout = stdout(); terminal::enable_raw_mode()?; execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?; let mut time = SystemTime::now(); let mut term = Terminal::new(&cfg); while is_running { if event::poll(Duration::ZERO)? { match event::read()? { event::Event::Key(e) => { if e.code == KeyCode::Esc { is_running = false; } } event::Event::Resize(_, _) => { fm.reset(); term.reinit(&cfg); } _ => {} }; } (_width, _height) = terminal::size()?; let delta_time = SystemTime::now().duration_since(time).unwrap(); if fm.install_form == FireworkInstallForm::DynamicInstall { dyn_gen( &mut fm, if cfg.enable_cjk { (_width - 1) / 2 } else { _width }, _height, cli.gradient, &cfg, ); } fm.update(time, delta_time); time = SystemTime::now(); term.render(&fm, &cfg); term.print(&mut stdout, &cfg); if delta_time < Duration::from_secs_f32(1. / fps as f32) { let rem = Duration::from_secs_f32(1. / fps as f32) - delta_time; sleep(rem); } } execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?; terminal::disable_raw_mode()?; Ok(()) } ================================================ FILE: src/config.rs ================================================ /// Configuration of the program #[derive(Default)] pub struct Config { pub enable_cjk: bool, } ================================================ FILE: src/demo.rs ================================================ //! This module provides some demos of different types of fireworks use std::{ f32::consts::PI, time::{Duration, SystemTime}, }; use glam::Vec2; use rand::{seq::IteratorRandom, thread_rng, Rng}; use crate::{ config::Config, fireworks::{ExplosionForm, Firework, FireworkConfig}, particle::ParticleConfig, utils::{ explosion_gradient_1, explosion_gradient_2, explosion_gradient_3, gen_points_arc, gen_points_circle, gen_points_circle_normal, gen_points_circle_normal_dev, gen_points_fan, linear_gradient_1, }, }; pub fn demo_firework_0( center: Vec2, spawn_after: Duration, enable_gradient: bool, colors: Vec<(u8, u8, u8)>, cfg: &Config, ) -> Firework { let mut particles = Vec::new(); for v in gen_points_circle_normal( thread_rng().gen_range(if cfg.enable_cjk { 400.0..600.0 } else { 230.0..400.0 }), thread_rng().gen_range(if cfg.enable_cjk { 20..35 } else { 33..47 }), ) .iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(20..25), Duration::from_secs_f32(thread_rng().gen_range(1.8..2.3)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default().with_gradient_scale(explosion_gradient_1); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } } pub fn demo_firework_1(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework { let colors = [ (255, 102, 75), (144, 56, 67), (255, 225, 124), (206, 32, 41), ]; let mut particles = Vec::new(); for v in gen_points_circle_normal(250., 45).iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(23..27), Duration::from_secs_f32(thread_rng().gen_range(2.1..2.7)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default().with_gradient_scale(explosion_gradient_1); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } } pub fn demo_firework_2(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework { let colors = [(250, 216, 68)]; let mut particles = Vec::new(); for v in gen_points_circle(100, 600).iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(5..8), Duration::from_secs_f32(thread_rng().gen_range(3.0..5.5)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_gradient_scale(explosion_gradient_2) .with_gravity_scale(0.) .with_ar_scale(0.15); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } } pub fn demo_firework_3(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework { let colors = [ (242, 233, 190), (226, 196, 136), (149, 202, 176), (26, 64, 126), ]; let mut particles = Vec::new(); for v in gen_points_circle_normal(350., 135).iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(23..43), Duration::from_secs_f32(thread_rng().gen_range(3.5..5.0)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_gradient_scale(explosion_gradient_1) .with_ar_scale(0.18) .with_gravity_scale(0.7); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } } pub fn demo_firework_4(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework { let colors = [(242, 233, 190), (226, 196, 136), (255, 248, 253)]; let mut particles = Vec::new(); for v in gen_points_circle_normal(350., 25).iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(20..33), Duration::from_secs_f32(thread_rng().gen_range(3.5..5.0)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_gradient_scale(explosion_gradient_1) .with_gravity_scale(0.3); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } } pub fn demo_firework_5(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework { let colors = [(152, 186, 227), (54, 84, 117), (21, 39, 60)]; let mut particles = Vec::new(); for v in gen_points_circle_normal(450., 80).iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(33..43), Duration::from_secs_f32(thread_rng().gen_range(3.5..5.0)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_gradient_scale(explosion_gradient_3) .with_gravity_scale(1.4); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } } pub fn demo_firework_6(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework { let colors = [(242, 233, 190), (226, 196, 136), (255, 248, 253)]; let mut particles = Vec::new(); for v in gen_points_circle_normal(350., 35).iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(20..23), Duration::from_secs_f32(thread_rng().gen_range(3.5..4.0)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_gradient_scale(explosion_gradient_1) .with_ar_scale(0.19) .with_gravity_scale(0.1); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } } pub fn demo_firework_comb_1( start: Vec2, spawn_after: Duration, enable_gradient: bool, ) -> Vec { // Ascent of rocket let color1 = (255, 255, 235); let particles1 = ParticleConfig::new( start, Vec2::NEG_Y * 160., 6, Duration::from_secs_f32(1.2), color1, ); let mut config1 = FireworkConfig::default() .with_ar_scale(0.04) .with_gradient_scale(linear_gradient_1); config1.set_enable_gradient(enable_gradient); // Explosion let color2 = [ (235, 39, 155), (250, 216, 68), (242, 52, 72), (63, 52, 200), (255, 139, 57), ]; let center2 = start + Vec2::NEG_Y * 53.; let mut particles2 = Vec::new(); for v in gen_points_circle_normal(350., 160).iter() { particles2.push(ParticleConfig::new( center2, *v, thread_rng().gen_range(23..43), Duration::from_secs_f32(thread_rng().gen_range(2.5..4.5)), *color2.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config2 = FireworkConfig::default() .with_gradient_scale(explosion_gradient_1) .with_ar_scale(0.2) .with_gravity_scale(0.3); config2.set_enable_gradient(enable_gradient); vec![ Firework { init_time: SystemTime::now(), spawn_after, center: start, particles: vec![particles1], config: config1, ..Default::default() }, Firework { init_time: SystemTime::now(), spawn_after: spawn_after + Duration::from_secs_f32(1.2), center: center2, particles: particles2, config: config2, ..Default::default() }, ] } pub fn demo_firework_comb_0( center: Vec2, spawn_after: Duration, enable_gradient: bool, ) -> Vec { vec![ demo_firework_3(center + Vec2::new(-5., -19.), spawn_after, enable_gradient), demo_firework_4( center + Vec2::new(-30., 0.), spawn_after + Duration::from_secs_f32(0.4), enable_gradient, ), demo_firework_5( center + Vec2::new(12., 0.), spawn_after + Duration::from_secs_f32(1.6), enable_gradient, ), demo_firework_1( center + Vec2::new(-9., 7.), spawn_after + Duration::from_secs_f32(2.), enable_gradient, ), demo_firework_6( center + Vec2::new(24., -11.), spawn_after + Duration::from_secs_f32(2.3), enable_gradient, ), ] } pub fn demo_firework_comb_2( center: Vec2, spawn_after: Duration, enable_gradient: bool, ) -> Vec { let mut res = Vec::new(); let fountain1 = |center: Vec2, angle: f32| { let colors = [(255, 183, 3), (251, 133, 0), (242, 233, 190)]; let mut particles = Vec::new(); for v in gen_points_fan(60., 20, angle - 0.05, angle + 0.05).iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(28..38), Duration::from_secs_f32(thread_rng().gen_range(2.5..3.8)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_ar_scale(0.05) .with_gradient_scale(linear_gradient_1); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, form: ExplosionForm::Sustained { lasts: Duration::from_secs(5), time_interval: Duration::from_secs_f32(0.08), timer: Duration::ZERO, }, ..Default::default() } }; let fountain2 = |center: Vec2| { let colors = [(226, 196, 136), (255, 245, 253), (208, 58, 99)]; let mut particles = Vec::new(); for v in gen_points_fan(1000., 20, 5.7 / 12. * PI, 6.3 / 12. * PI).iter() { particles.push(ParticleConfig::new( center, *v, thread_rng().gen_range(28..38), Duration::from_secs_f32(thread_rng().gen_range(2.5..3.8)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_ar_scale(0.14) .with_gravity_scale(0.9) .with_gradient_scale(linear_gradient_1); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after: spawn_after + Duration::from_secs_f32(4.), center, particles, config, form: ExplosionForm::Sustained { lasts: Duration::from_secs(5), time_interval: Duration::from_secs_f32(0.08), timer: Duration::ZERO, }, ..Default::default() } }; let mono = |center: Vec2, sa: Duration, colors: Vec<(u8, u8, u8)>| { let particles = vec![ParticleConfig::new( center, gen_points_arc(200., 1, 5. / 12. * PI, 7. / 12. * PI)[0], thread_rng().gen_range(24..30), Duration::from_secs_f32(thread_rng().gen_range(2.1..2.7)), *colors.iter().choose(&mut thread_rng()).unwrap(), )]; let mut config = FireworkConfig::default() .with_ar_scale(thread_rng().gen_range(0.18..0.24)) .with_gradient_scale(linear_gradient_1); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after: sa, center, particles, config, form: ExplosionForm::Instant { used: false }, ..Default::default() } }; res.push(fountain1(center + Vec2::new(-31., 21.), 1.05)); res.push(fountain1(center + Vec2::new(31., 21.), 2.09)); res.push(fountain2(center + Vec2::new(-7., 21.))); res.push(fountain2(center + Vec2::new(7., 21.))); (-33..=33).step_by(3).for_each(|i| { res.push(mono( Vec2::new(center.x + i as f32, center.y + 21.), Duration::from_secs_f32(3.5), vec![(0, 119, 182), (144, 224, 239), (12, 180, 216)], )) }); (-33..=33).step_by(3).for_each(|i| { res.push(mono( Vec2::new(center.x + i as f32, center.y + 21.), Duration::from_secs_f32(4.7), vec![(181, 23, 158), (247, 37, 133), (114, 9, 183)], )) }); (-33..=33).step_by(3).for_each(|i| { res.push(mono( Vec2::new(center.x + i as f32, center.y + 21.), Duration::from_secs_f32(5.9), vec![(217, 237, 146), (153, 217, 140), (82, 182, 154)], )) }); res } pub fn demo_firework_comb_3( center: Vec2, spawn_after: Duration, enable_gradient: bool, ) -> Vec { let mut res = Vec::new(); let f1 = { let colors = [(255, 216, 190), (255, 238, 221), (248, 247, 255)]; let mut particles = Vec::new(); for v in gen_points_circle_normal_dev(14., 200, 60.).iter() { particles.push(ParticleConfig::new( center + Vec2::NEG_Y * 6., *v, thread_rng().gen_range(15..20), Duration::from_secs_f32(thread_rng().gen_range(3.0..5.0)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_gradient_scale(explosion_gradient_1) .with_ar_scale(0.15) .with_gravity_scale(0.35); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } }; res.push(f1); let f2 = { let colors = [ (152, 186, 227), (89, 129, 177), (54, 84, 117), (240, 244, 254), ]; let mut particles = Vec::new(); for v in gen_points_circle_normal_dev(10000., 600, 30.).iter() { particles.push(ParticleConfig::new( center + Vec2::NEG_Y * 6., *v, thread_rng().gen_range(20..28), Duration::from_secs_f32(thread_rng().gen_range(4.8..10.)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_gradient_scale(explosion_gradient_1) .with_ar_scale(0.09) .with_gravity_scale(0.5); config.set_enable_gradient(enable_gradient); Firework { init_time: SystemTime::now(), spawn_after, center, particles, config, ..Default::default() } }; res.push(f2); for (idx, p) in gen_points_circle(27, 10).iter().enumerate() { let colors = [(17, 138, 178), (6, 214, 160), (7, 59, 76), (255, 255, 255)]; let mut particles = Vec::new(); for v in gen_points_circle_normal_dev(100., 35, 350. / 9.).iter() { particles.push(ParticleConfig::new( center + *p, *v, thread_rng().gen_range(20..30), Duration::from_secs_f32(thread_rng().gen_range(3.0..4.0)), *colors.iter().choose(&mut thread_rng()).unwrap(), )); } let mut config = FireworkConfig::default() .with_gradient_scale(explosion_gradient_3) .with_ar_scale(0.28) .with_gravity_scale(0.25); config.set_enable_gradient(enable_gradient); res.push(Firework { init_time: SystemTime::now(), spawn_after: spawn_after + Duration::from_secs_f32(0.2 * (idx + 4) as f32), center, particles, config, ..Default::default() }); } res } ================================================ FILE: src/fireworks.rs ================================================ //! `firework` module provides functions to define, create and update fireworks use std::{ collections::VecDeque, time::{Duration, SystemTime}, }; use glam::Vec2; use rand::{seq::IteratorRandom, thread_rng}; use crate::particle::{LifeState, Particle, ParticleConfig}; /// Struct representing a single firework pub struct Firework { /// The `SystemTime` when the object is initialized/defined pub init_time: SystemTime, /// Firework spawns after `spawn_after` from `init_time` pub spawn_after: Duration, pub time_elapsed: Duration, pub center: Vec2, pub state: FireworkState, pub config: FireworkConfig, pub form: ExplosionForm, pub particles: Vec, pub current_particles: Vec, } impl Default for Firework { fn default() -> Self { Self { init_time: SystemTime::now(), spawn_after: Duration::ZERO, time_elapsed: Duration::ZERO, center: Vec2::ZERO, state: FireworkState::Waiting, config: FireworkConfig::default(), form: ExplosionForm::Instant { used: false }, particles: Vec::new(), current_particles: Vec::new(), } } } impl Firework { /// Update the `Firework` /// /// # Arguments /// /// * `now` - `SystemTime` of now /// * `delta_time` - `Duration` since last update pub fn update(&mut self, now: SystemTime, delta_time: Duration) { // Spawn particles if now >= self.init_time + self.spawn_after { self.time_elapsed += delta_time; match &mut self.form { ExplosionForm::Instant { used } => { if !*used { self.particles.iter().for_each(|p| { self.current_particles.push(Particle { pos: p.init_pos, vel: p.init_vel, trail: init_trail(p.init_pos, p.trail_length), life_state: LifeState::Alive, time_elapsed: Duration::ZERO, config: *p, }) }) } *used = true; } ExplosionForm::Sustained { lasts, time_interval, timer, } => { if self.time_elapsed <= *lasts { if *timer + delta_time <= *time_interval { *timer += delta_time; } else { let n = (*timer + delta_time).as_millis() / (*time_interval).as_millis(); self.particles .iter() .choose_multiple(&mut thread_rng(), n as usize) .iter() .for_each(|p| { self.current_particles.push(Particle { pos: p.init_pos, vel: p.init_vel, trail: init_trail(p.init_pos, p.trail_length), life_state: LifeState::Alive, time_elapsed: Duration::ZERO, config: **p, }) }); *timer = Duration::from_millis( ((*timer + delta_time).as_millis() % (*time_interval).as_millis()) as u64, ); } } } } self.state = FireworkState::Alive; } self.current_particles .iter_mut() .for_each(|p| p.update(delta_time, &self.config)); // Clean the dead pariticles self.current_particles .retain(|p| p.life_state != LifeState::Dead); match self.form { ExplosionForm::Instant { used } => { if used && self.state == FireworkState::Alive && self.current_particles.is_empty() { self.state = FireworkState::Gone; } } ExplosionForm::Sustained { lasts, .. } => { if self.time_elapsed > lasts && self.state == FireworkState::Alive && self.current_particles.is_empty() { self.state = FireworkState::Gone; } } } } /// Return true if the `FireworkState` is `Gone` pub fn is_gone(&self) -> bool { self.state == FireworkState::Gone } /// Reset `Firework` to its initial state pub fn reset(&mut self) { self.init_time = SystemTime::now(); self.state = FireworkState::Waiting; self.time_elapsed = Duration::ZERO; self.current_particles = Vec::new(); match &mut self.form { ExplosionForm::Instant { used } => { *used = false; } ExplosionForm::Sustained { timer, .. } => { *timer = Duration::ZERO; } } } } /// Struct representing state of a `Firework` /// /// State goes from `Waiting` -> `Alive` -> `Gone` /// /// # Notes /// /// - `Firework` turns to `Alive` when it is spawned /// - `Firework` turns to `Gone` when all of its `Particles` are `Dead` #[derive(Debug, PartialEq, Default)] pub enum FireworkState { #[default] Waiting, Alive, Gone, } /// Enum that represents whether the `Firework` make one instantaneous explosion or continuously emit particles #[derive(Debug, PartialEq, Eq)] pub enum ExplosionForm { Instant { used: bool, }, Sustained { /// `Duration` that the sustained firework will last lasts: Duration, /// Time interval between two particle spawn time_interval: Duration, timer: Duration, }, } /// Struct representing the configuration of a single `Firework` /// /// This applies to all `Particle` in the `Firework` pub struct FireworkConfig { /// Larger `gravity_scale` tends to pull particles down pub gravity_scale: f32, /// Air resistance scale /// Warning: too large or too small `ar_scale` may lead to unexpected behavior of `Particles` pub ar_scale: f32, pub additional_force: Box Vec2>, /// This field is a function that takes a float between 0 and 1, returns a float representing all `Particle`s' gradient /// /// `Particle`s' gradient changes according to its elapsed time and lifetime /// The input `f32` equals to `time_elapsed`/`life_time`, which returns a `f32` affecting its color gradient /// `gradient_scale` returns 1. means`Particle` will have the same colors as defined all over its lifetime pub gradient_scale: fn(f32) -> f32, /// Set wheter or not firework has color gradient /// /// # Notes /// /// - It is recommanded that your terminal window is non-transparent and has black bg color to get better visual effects /// - Otherwise set it to `false` pub enable_gradient: bool, } impl Default for FireworkConfig { fn default() -> Self { Self { gravity_scale: 1., ar_scale: 0.28, additional_force: Box::new(move |_| Vec2::ZERO), gradient_scale: |_| 1., enable_gradient: false, } } } impl FireworkConfig { /// Set `gradient_scale` #[inline] #[must_use] pub fn with_gradient_scale(mut self, f: fn(f32) -> f32) -> Self { self.gradient_scale = f; self } /// Set `gravity_scale` #[inline] #[must_use] pub fn with_gravity_scale(mut self, s: f32) -> Self { self.gravity_scale = s; self } /// Set `ar_scale` #[inline] #[must_use] pub fn with_ar_scale(mut self, s: f32) -> Self { self.ar_scale = s; self } /// Set `additional_force` #[inline] #[must_use] pub fn with_additional_force(mut self, af: impl Fn(&Particle) -> Vec2 + 'static) -> Self { self.additional_force = Box::new(af); self } /// Set `enable_gradient` pub fn set_enable_gradient(&mut self, enable_gradient: bool) { self.enable_gradient = enable_gradient; } } /// `FireworkManager` manages all `Firework`s pub struct FireworkManager { pub fireworks: Vec, /// If this is `true`, the whole fireworks show will restart when all the `Firework`s are `Gone` pub enable_loop: bool, /// Controls how fireworks are installed in `FireworkManager` pub install_form: FireworkInstallForm, } impl Default for FireworkManager { fn default() -> Self { Self { fireworks: Vec::new(), enable_loop: false, install_form: FireworkInstallForm::StaticInstall, } } } impl FireworkManager { /// Create a new `FireworkManager` with `enable_loop` set to `false` pub fn new(fireworks: Vec) -> Self { Self { fireworks, enable_loop: false, install_form: FireworkInstallForm::StaticInstall, } } /// Add a `Firework` to a existing `FireworkManager` pub fn add_firework(&mut self, firework: Firework) { self.fireworks.push(firework); } /// Add `Firework`s to a existing `FireworkManager` pub fn add_fireworks(&mut self, mut fireworks: Vec) { self.fireworks.append(&mut fireworks); } /// Add a `Firework` to `FireworkManager` #[inline] #[must_use] pub fn with_firework(mut self, firework: Firework) -> Self { self.fireworks.push(firework); self } // Add a vector of `Firework`s to `FireworkManager` #[inline] #[must_use] pub fn with_fireworks(mut self, mut fireworks: Vec) -> Self { self.fireworks.append(&mut fireworks); self } /// Set `enable_loop` to `true` #[inline] #[must_use] pub fn enable_loop(mut self) -> Self { self.enable_loop = true; self } /// Set `enable_loop` to `false` #[inline] #[must_use] pub fn disable_loop(mut self) -> Self { self.enable_loop = false; self } /// Reset the whole fireworks show pub fn reset(&mut self) { for ele in self.fireworks.iter_mut() { ele.reset(); } } pub fn set_enable_loop(&mut self, enable_loop: bool) { self.enable_loop = enable_loop; } /// The main update function pub fn update(&mut self, now: SystemTime, delta_time: Duration) { for ele in self.fireworks.iter_mut() { ele.update(now, delta_time); } if self.install_form == FireworkInstallForm::DynamicInstall { self.fireworks.retain(|f| f.state != FireworkState::Gone); } if self.install_form == FireworkInstallForm::StaticInstall && self.enable_loop && self.fireworks.iter().all(|f| f.is_gone()) { self.reset(); } } /// Set `install_form` to `DynamicInstall` pub fn enable_dyn_install(mut self) -> Self { self.install_form = FireworkInstallForm::DynamicInstall; self } } /// `StaticInstall` keeps all the fireworks in `FireworkManager` and won't delete them /// /// `DynamicInstall` automatically remove fireworks that are `Gone`, which let you add fireworks continuously /// /// # Notes /// /// - `FireworkManager` that has `DynamicInstall` can't loop, it will ignore the set `enable_loop` value #[derive(Debug, PartialEq)] pub enum FireworkInstallForm { StaticInstall, DynamicInstall, } fn init_trail(init_pos: Vec2, n: usize) -> VecDeque { VecDeque::from(vec![init_pos; n]) } ================================================ FILE: src/lib.rs ================================================ pub mod config; pub mod demo; pub mod fireworks; pub mod particle; pub mod term; pub mod utils; ================================================ FILE: src/particle.rs ================================================ //! `particle` module provides functions to define, create and update particles use std::{collections::VecDeque, time::Duration}; use glam::Vec2; use crate::fireworks::FireworkConfig; /// The struct represents the states in a `Particle`'s lifetime /// /// Every `Particle` goes from `Alive` -> `Declining` -> `Dying` -> `Dead` #[derive(Debug, Clone, Copy, PartialEq)] pub enum LifeState { Alive, Declining, Dying, Dead, } /// The struct representing a single particle #[derive(Debug, Clone)] pub struct Particle { pub pos: Vec2, pub vel: Vec2, /// Records a `trail_length` of previous positions of the `Particle` pub trail: VecDeque, pub life_state: LifeState, /// `Duration` since initialization of this `Particle` pub time_elapsed: Duration, pub config: ParticleConfig, } impl Default for Particle { fn default() -> Self { Self { pos: Vec2::ZERO, vel: Vec2::ZERO, trail: VecDeque::new(), life_state: LifeState::Alive, time_elapsed: Duration::ZERO, config: ParticleConfig::default(), } } } impl Particle { /// Create a new `Particle` pub fn new( pos: Vec2, vel: Vec2, trail_length: usize, life_time: Duration, color: (u8, u8, u8), ) -> Self { let trail = VecDeque::from(vec![pos; trail_length]); let life_state = LifeState::Alive; Self { pos, vel, trail, life_state, time_elapsed: Duration::ZERO, config: ParticleConfig::new(pos, vel, trail_length, life_time, color), } } /// Return true if `Particle`'s `LifeState` is `Dead` pub fn is_dead(&self) -> bool { self.life_state == LifeState::Dead } /// Reset `Particle` to its initial state pub fn reset(&mut self) { self.pos = self.config.init_pos; self.vel = self.config.init_vel; (0..self.config.trail_length).for_each(|i| self.trail[i] = self.pos); self.life_state = LifeState::Alive; self.time_elapsed = Duration::ZERO; } /// Update the `Particle` based on delta time /// /// # Arguments /// /// * - `duration` - `Duration` since last update pub fn update(&mut self, duration: Duration, config: &FireworkConfig) { const TIME_STEP: f32 = 0.001; self.time_elapsed += duration; self.life_state = cal_life_state(self.config.life_time, self.time_elapsed); let mut t = 0.; while t < duration.as_secs_f32() { self.vel += TIME_STEP * (Vec2::Y * 10. * config.gravity_scale - self.vel.normalize() * self.vel.length().powi(2) * config.ar_scale + (config.additional_force)(self)); self.pos += TIME_STEP * self.vel; t += TIME_STEP; } self.trail.pop_front(); self.trail.push_back(self.pos); } } /// Struct that defines the configuration of `Particle` #[derive(Debug, Copy, Clone, PartialEq)] pub struct ParticleConfig { pub init_pos: Vec2, pub init_vel: Vec2, pub trail_length: usize, /// `Duration` from `Particle`'s initialization to its `Dead` pub life_time: Duration, /// Color in RGB (from 0 to 255) pub color: (u8, u8, u8), } impl Default for ParticleConfig { fn default() -> Self { Self { init_pos: Vec2::ZERO, init_vel: Vec2::ZERO, trail_length: 2, life_time: Duration::from_secs(3), color: (255, 255, 255), } } } impl ParticleConfig { /// Create a new `ParticleConfig` pub fn new( init_pos: Vec2, init_vel: Vec2, trail_length: usize, life_time: Duration, color: (u8, u8, u8), ) -> Self { Self { init_pos, init_vel, trail_length, life_time, color, } } } fn cal_life_state(life_time: Duration, current_elapsed: Duration) -> LifeState { let p = current_elapsed.as_millis() as f32 / life_time.as_millis() as f32; if p < 0.4 { LifeState::Alive } else if p < 0.65 { LifeState::Declining } else if p < 1. { LifeState::Dying } else { LifeState::Dead } } ================================================ FILE: src/term.rs ================================================ //! `term` module provides functions of rendering in terminal use std::io::{Stdout, Write}; use crossterm::{cursor::MoveTo, queue, style, terminal}; use glam::Vec2; use rand::{seq::IteratorRandom, thread_rng}; use crate::{ config::Config, fireworks::{FireworkManager, FireworkState}, particle::LifeState, utils::distance_squared, }; /// Wrap a character with color #[derive(Debug, Clone, Copy)] pub struct Char { pub text: char, pub color: style::Color, } #[allow(unused)] impl Char { /// Create a new `Char` fn new(text: char, color: style::Color) -> Self { Self { text, color } } } /// Struct that represents a terminal pub struct Terminal { pub size: (u16, u16), pub screen: Vec>, } impl Default for Terminal { fn default() -> Self { let size = terminal::size().expect("Fail to get terminal size."); let screen = vec![ vec![ Char { text: ' ', color: style::Color::White }; size.0 as usize ]; size.1 as usize ]; Self { size, screen } } } impl Terminal { pub fn new(cfg: &Config) -> Self { let mut size = terminal::size().expect("Fail to get terminal size."); if cfg.enable_cjk { size.0 = (size.0 - 1) / 2; } let screen = vec![ vec![ Char { text: ' ', color: style::Color::White }; size.0 as usize ]; size.1 as usize ]; Self { size, screen } } /// Reload terminal to adapt new window size pub fn reinit(&mut self, cfg: &Config) { let mut size = terminal::size().expect("Fail to get terminal size."); if cfg.enable_cjk { size.0 = (size.0 - 1) / 2; } self.size = size; self.screen = vec![ vec![ Char { text: ' ', color: style::Color::White }; size.0 as usize ]; size.1 as usize ]; } /// Clear the terminal screen by setting all the characters in terminal to space pub fn clear_screen(&mut self) { let size = terminal::size().expect("Fail to get terminal size."); self.screen = vec![ vec![ Char { text: ' ', color: style::Color::White }; size.0 as usize ]; size.1 as usize ]; } /// Print the data out to terminal pub fn print(&self, w: &mut Stdout, cfg: &Config) { self.screen.iter().enumerate().for_each(|(y, line)| { line.iter().enumerate().for_each(|(x, c)| { queue!( w, MoveTo( if cfg.enable_cjk { (x * 2) as u16 } else { x as u16 }, y as u16 ), style::SetForegroundColor(c.color), style::Print(c.text) ) .expect("Std io error.") }); }); w.flush().expect("Std io error."); } /// Write the rendering data of all `Fireworks` and `Particles` to `Terminal` pub fn render(&mut self, fm: &FireworkManager, cfg: &Config) { self.clear_screen(); for firework in fm.fireworks.iter().rev() { if firework.state == FireworkState::Alive { for particle in firework.current_particles.iter().rev() { let grad = if firework.config.enable_gradient { Some((firework.config.gradient_scale)( particle.time_elapsed.as_secs_f32() / particle.config.life_time.as_secs_f32(), )) } else { None }; particle .trail .iter() .map(|p| { if cfg.enable_cjk { *p } else { Vec2::new(p.x * 2., p.y) } }) .rev() .collect::>() .windows(2) .enumerate() .for_each(|(idx, v)| { let density = (particle.config.trail_length - idx - 1) as f32 / particle.config.trail_length as f32; construct_line(v[0], v[1]).iter().for_each(|p| { if self.inside(*p) && self.screen[p.1 as usize][p.0 as usize].text == ' ' { if let Some(c) = match particle.life_state { LifeState::Alive => { Some(get_char_alive(density, cfg.enable_cjk)) } LifeState::Declining => { Some(get_char_declining(density, cfg.enable_cjk)) } LifeState::Dying => { Some(get_char_dying(density, cfg.enable_cjk)) } LifeState::Dead => None, } { self.screen[p.1 as usize][p.0 as usize] = Char { text: c, color: { let color_u8 = if let Some(g) = grad { shift_gradient(particle.config.color, g) } else { particle.config.color }; style::Color::Rgb { r: color_u8.0, g: color_u8.1, b: color_u8.2, } }, } } } }); }); } } } } fn inside(&self, (x, y): (isize, isize)) -> bool { x < self.size.0 as isize && y < self.size.1 as isize && x >= 0 && y >= 0 } } fn construct_line(a: Vec2, b: Vec2) -> Vec<(isize, isize)> { const STEP: f32 = 0.2; let (x0, y0) = (a.x, a.y); let (x1, y1) = (b.x, b.y); let mut path = Vec::new(); let mut x = x0; let mut y = y0; let slope = (y1 - y0) / (x1 - x0); let dx = if x0 == x1 { 0. } else if x1 > x0 { 1. } else { -1. }; let dy = if y0 == y1 { 0. } else if y1 > y0 { 1. } else { -1. }; let mut ds = distance_squared(a, b) + f32::EPSILON; path.push((x0.round() as isize, y0.round() as isize)); if (x1 - x0).abs() >= (y1 - y0).abs() { while distance_squared(Vec2::new(x, y), b) <= ds { if *path.last().unwrap() != (x.round() as isize, y.round() as isize) { path.push((x.round() as isize, y.round() as isize)); ds = distance_squared(Vec2::new(x, y), b); } x += dx * STEP; y += dy * (STEP * slope).abs(); } } else { while distance_squared(Vec2::new(x, y), b) <= ds { if *path.last().unwrap() != (x.round() as isize, y.round() as isize) { path.push((x.round() as isize, y.round() as isize)); ds = distance_squared(Vec2::new(x, y), b); } y += dy * STEP; x += dx * (STEP / slope).abs(); } } path } fn shift_gradient(color: (u8, u8, u8), scale: f32) -> (u8, u8, u8) { ( (color.0 as f32 * scale) as u8, (color.1 as f32 * scale) as u8, (color.2 as f32 * scale) as u8, ) } fn get_char_alive(density: f32, cjk: bool) -> char { let palette = if density < 0.3 { if cjk { "。,”“』 『¥" } else { "`'. " } } else if density < 0.5 { if cjk { "一二三二三五十十已于上下义天" // "いうよへくひとフーク " } else { "/\\|()1{}[]?" } } else if density < 0.7 { if cjk { "时中自字木月日目火田左右点以" // "探しているのが誰かなのかどこかなのかそれともただ単に就職先なのか自分でもよくわからない" } else { "oahkbdpqwmZO0QLCJUYXzcvunxrjft*" } } else if cjk { "龖龠龜" // "東京福岡横浜縄" } else { "$@B%8&WM#" }; palette .chars() .choose(&mut thread_rng()) .expect("Fail to choose character.") } fn get_char_declining(density: f32, cjk: bool) -> char { let palette = if density < 0.2 { if cjk { "?。, 『』 ||" } else { "` '. " } } else if density < 0.6 { if cjk { "()【】*¥|十一二三六" // "()【】*¥|ソファー" } else { "-_ +~<> i!lI;:,\"^" } } else if density < 0.85 { if cjk { "人中亿入上下火土" // "人ならざるものに出会うかもしれない" } else { "/\\| ()1{}[ ]?" } } else if cjk { "繁荣昌盛国泰民安龍龖龠龜耋" // "時間言葉目覚" } else { "xrjft*" }; palette .chars() .choose(&mut thread_rng()) .expect("Fail to choose character.") } fn get_char_dying(density: f32, cjk: bool) -> char { let palette = if density < 0.6 { if cjk { "。 『 』 、: |。,— ……" } else { ". ,`. ^,' . " } } else if cjk { "|¥人 上十入乙小 下" // "イントマトナイフ" } else { " /\\| ( ) 1{} [ ]?i !l I;: ,\"^ " }; palette .chars() .choose(&mut thread_rng()) .expect("Fail to choose character.") } ================================================ FILE: src/utils.rs ================================================ //! `utils` module provides some useful helper functions of random generation and gradient scale use std::f32::consts::PI; use glam::Vec2; use rand::Rng; use rand_distr::Distribution; /// Round a `Vec2` from `(f32, f32)` to `(isize, isize)` pub fn round(input: Vec2) -> (isize, isize) { (input.x.round() as isize, input.y.round() as isize) } /// Generate random `Vec2` within a circle range pub fn gen_points_circle(radius: isize, n: usize) -> Vec { let mut res = Vec::new(); while res.len() < n { let x = rand::thread_rng().gen_range(-radius..=radius); let y = rand::thread_rng().gen_range(-radius..=radius); if x.pow(2) + y.pow(2) <= radius.pow(2) { res.push(Vec2::new(x as f32, y as f32)); } } res } /// Generate random `Vec2` within a circle range with normal distribution /// /// Points closer to the center will be denser pub fn gen_points_circle_normal(radius: f32, n: usize) -> Vec { let mut rng = rand::thread_rng(); let normal = rand_distr::Normal::new(0., radius / 9.).expect("Unable to generate normal distribution."); let mut res = Vec::new(); while res.len() < n { let x = normal.sample(&mut rng); if x < -radius || x > radius { continue; } let y = normal.sample(&mut rng); if x < -radius || y > radius { continue; } if x.powi(2) + y.powi(2) <= radius.powi(2) { res.push(Vec2::new(x, y)); } } res } /// Generate random `Vec2` within a circle range with normal distribution /// /// Points closer to the center will be denser /// You can specify standard deviation yourself pub fn gen_points_circle_normal_dev(radius: f32, n: usize, std_dev: f32) -> Vec { let mut rng = rand::thread_rng(); let normal = rand_distr::Normal::new(0., std_dev).expect("Unable to generate normal distribution."); let mut res = Vec::new(); while res.len() < n { let x = normal.sample(&mut rng); if x < -radius || x > radius { continue; } let y = normal.sample(&mut rng); if x < -radius || y > radius { continue; } if x.powi(2) + y.powi(2) <= radius.powi(2) { res.push(Vec2::new(x, y)); } } res } /// Generate random `Vec2` within a fan-shape range pub fn gen_points_fan(radius: f32, n: usize, st_angle: f32, ed_angle: f32) -> Vec { let mut res = Vec::new(); while res.len() < n { let x = rand::thread_rng().gen_range(-radius..=radius); let y = rand::thread_rng().gen_range(-radius..=radius); let t = y.atan2(x); if t <= ed_angle && t >= st_angle && x.powi(2) + y.powi(2) <= radius.powi(2) { res.push(Vec2::new(x, -y)); } } res } /// Generate random `Vec2` on an arc pub fn gen_points_arc(radius: f32, n: usize, st_angle: f32, ed_angle: f32) -> Vec { let mut res = Vec::new(); while res.len() < n { let a = rand::thread_rng().gen_range(st_angle..=ed_angle); res.push(Vec2::new(radius * a.cos(), -radius * a.sin())); } res } /// Generate random `Vec2` on a circle pub fn gen_points_on_circle(radius: f32, n: usize) -> Vec { let mut res = Vec::new(); while res.len() < n { let a = rand::thread_rng().gen_range(0.0..PI); res.push(Vec2::new(radius * a.cos(), -radius * a.sin())); } res } /// Return squared distance between to points pub fn distance_squared(a: Vec2, b: Vec2) -> f32 { (b.x - a.x).powi(2) + (b.y - a.y).powi(2) } /// A sample function defining the gradient of the `Particle` /// /// The visual effect is similar to an explosion pub fn explosion_gradient_1(x: f32) -> f32 { if x < 0.087 { 150. * x.powi(2) } else { -0.8 * x + 1.2 } } /// A sample function defining the gradient of the `Particle` /// /// The visual effect is similar to an explosion pub fn explosion_gradient_2(x: f32) -> f32 { if x < 0.067 { 5. * x + 0.1 } else if x < 0.2 { 2. * x + 0.3 } else if x < 0.5 { x + 0.5 } else if x < 0.684 { 0.5 * x + 0.75 } else { -7. * (x - 0.65).powi(2) + 1.1 } } /// A sample function defining the gradient of the `Particle` /// /// The visual effect is similar to an explosion, darkar than `explosion_gradient_1` pub fn explosion_gradient_3(x: f32) -> f32 { if x < 0.087 { 150. * x.powi(2) * 0.6 } else { (-0.8 * x + 1.2) * 0.6 } } /// A sample function defining the gradient of the `Particle` /// /// Linear gradient pub fn linear_gradient_1(x: f32) -> f32 { -0.7 * x + 1. }