[
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\non:\n  pull_request:\n  push:\n    branches:\n      - master\n      - v0.3.0\n  schedule:\n    - cron: '00 01 * * *'\n\njobs:\n  check:\n    name: Check\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install stable toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n          override: true\n\n      - uses: Swatinem/rust-cache@v1\n\n      - name: Run cargo check\n        uses: actions-rs/cargo@v1\n        with:\n          command: check\n\n  test:\n    name: Test Suite\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n        rust: [stable]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install stable toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: ${{ matrix.rust }}\n          override: true\n\n      - uses: Swatinem/rust-cache@v1\n\n      - name: Run cargo test\n        uses: actions-rs/cargo@v1\n        with:\n          command: test\n\n\n  lints:\n    name: Lints\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n        with:\n          submodules: true\n\n      - name: Install stable toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n          override: true\n          components: rustfmt, clippy\n\n      - uses: Swatinem/rust-cache@v1\n\n      - name: Run cargo fmt\n        uses: actions-rs/cargo@v1\n        with:\n          command: fmt\n          args: --all -- --check\n\n      # - name: Run cargo clippy\n      #   uses: actions-rs/cargo@v1\n      #   with:\n      #     command: clippy\n      #     args: -- -D warnings"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non:\n  push:\n    tags:\n    - 'v[0-9]+.[0-9]+.[0-9]+'\n\nenv:\n  BIN_NAME: firework\n  PROJECT_NAME: firework-rs\n  REPO_NAME: Wayoung7/firework-rs\n\njobs:\n  dist:\n    name: Dist\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        build: [x86_64-linux, aarch64-linux, x86_64-macos, x86_64-windows] \n        include:\n        - build: x86_64-linux\n          os: ubuntu-20.04\n          rust: stable\n          target: x86_64-unknown-linux-gnu\n          cross: false\n        - build: aarch64-linux\n          os: ubuntu-20.04\n          rust: stable\n          target: aarch64-unknown-linux-gnu\n          cross: true\n        - build: x86_64-macos\n          os: macos-latest\n          rust: stable\n          target: x86_64-apple-darwin\n          cross: false\n        - build: x86_64-windows\n          os: windows-2019\n          rust: stable\n          target: x86_64-pc-windows-msvc\n          cross: false\n\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n        with:\n          submodules: true\n\n      - name: Install ${{ matrix.rust }} toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: ${{ matrix.rust }}\n          target: ${{ matrix.target }}\n          override: true\n\n      - name: Run cargo test\n        uses: actions-rs/cargo@v1\n        with:\n          use-cross: ${{ matrix.cross }}\n          command: test\n          args: --release --locked --target ${{ matrix.target }}\n\n      - name: Build release binary\n        uses: actions-rs/cargo@v1\n        with:\n          use-cross: ${{ matrix.cross }}\n          command: build\n          args: --release --locked --target ${{ matrix.target }}\n\n      - name: Strip release binary (linux and macos)\n        if: matrix.build == 'x86_64-linux' || matrix.build == 'x86_64-macos'\n        run: strip \"target/${{ matrix.target }}/release/$BIN_NAME\"\n\n      - name: Strip release binary (arm)\n        if: matrix.build == 'aarch64-linux'\n        run: |\n          docker run --rm -v \\\n            \"$PWD/target:/target:Z\" \\\n            rustembedded/cross:${{ matrix.target }} \\\n            aarch64-linux-gnu-strip \\\n            /target/${{ matrix.target }}/release/$BIN_NAME\n\n      - name: Build archive\n        shell: bash\n        run: |\n          mkdir dist\n          if [ \"${{ matrix.os }}\" = \"windows-2019\" ]; then\n            cp \"target/${{ matrix.target }}/release/$BIN_NAME.exe\" \"dist/\"\n          else\n            cp \"target/${{ matrix.target }}/release/$BIN_NAME\" \"dist/\"\n          fi\n\n      - uses: actions/upload-artifact@v2.2.4\n        with:\n          name: bins-${{ matrix.build }}\n          path: dist\n\n  publish:\n    name: Publish\n    needs: [dist]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n        with:\n          submodules: false\n\n      - uses: actions/download-artifact@v2\n      - run: ls -al bins-*\n\n      - name: Calculate tag name\n        run: |\n          name=dev\n          if [[ $GITHUB_REF == refs/tags/v* ]]; then\n            name=${GITHUB_REF:10}\n          fi\n          echo ::set-output name=val::$name\n          echo TAG=$name >> $GITHUB_ENV\n        id: tagname\n\n      - name: Build archive\n        shell: bash\n        run: |\n          set -ex\n\n          rm -rf tmp\n          mkdir tmp\n          mkdir dist\n\n          for dir in bins-* ; do\n              platform=${dir#\"bins-\"}\n              unset exe\n              if [[ $platform =~ \"windows\" ]]; then\n                  exe=\".exe\"\n              fi\n              pkgname=$PROJECT_NAME-$TAG-$platform\n              mkdir tmp/$pkgname\n              # cp LICENSE README.md tmp/$pkgname\n              mv bins-$platform/$BIN_NAME$exe tmp/$pkgname\n              chmod +x tmp/$pkgname/$BIN_NAME$exe\n\n              if [ \"$exe\" = \"\" ]; then\n                  tar cJf dist/$pkgname.tar.xz -C tmp $pkgname\n              else\n                  (cd tmp && 7z a -r ../dist/$pkgname.zip $pkgname)\n              fi\n          done\n\n      - name: Upload binaries to release\n        uses: svenstaro/upload-release-action@v2\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          file: dist/*\n          file_glob: true\n          tag: ${{ steps.tagname.outputs.val }}\n          overwrite: true\n\n      - name: Extract version\n        id: extract-version\n        run: |\n          printf \"::set-output name=%s::%s\\n\" tag-name \"${GITHUB_REF#refs/tags/}\""
  },
  {
    "path": ".gitignore",
    "content": "/target\n.vscode\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [0.3.1](https://github.com/Wayoung7/firework-rs/releases/tag/v0.3.1) - 2024-04-30\n\n### Changed\n - Performance enhanced by using `VecDeque` to represent trail and by using macro initialization of `Vec`\n\n## [0.3.0](https://github.com/Wayoung7/firework-rs/releases/tag/v0.3.0) - 2024-04-12\n\n### Added\n - CJK(also UTF-8) characters support (it takes twice of space as normal ascii characters)\n\n\n## [0.2.0](https://github.com/Wayoung7/firework-rs/releases/tag/v0.2.0) - 2024-03-22\n\n### Added\n - Firework demo which generates fireworks infinitely and randomly\n - Command line argument of changing frame rate\n - New field in `FireworkManager` to control the installation of `Firework`s\n\n### Changed\n - Implementation of filtering dead `Particle`s"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"firework-rs\"\nversion = \"0.3.1\"\nauthors = [\"Wayoung7 <https://github.com/Wayoung7>\"]\nedition = \"2021\"\ndescription = \"A cross-platform ascii-art firework simulator in terminal\"\nreadme = \"README.md\"\nrepository = \"https://github.com/Wayoung7/firework-rs\"\nlicense = \"MIT\"\nkeywords = [\"ascii\", \"fireworks\", \"terminal\", \"cli\", \"simulation\"]\ncategories = [\"command-line-utilities\"]\nexclude = [\"gif/*\"]\n\n[dependencies]\nclap = { version = \"4.5.2\", features = [\"derive\"] }\ncrossterm = \"0.27.0\"\nglam = \"0.25.0\"\nrand = \"0.8.5\"\nrand_distr = \"0.4.3\"\n\n[[bin]]\nname = \"firework\"\npath = \"src/bin/firework/main.rs\"\n\n[lib]\nname = \"firework_rs\"\npath = \"src/lib.rs\"\ncrate-type = [\"lib\"]\nbench = false\n\n[[example]]\nname = \"fountain\"\n\n[[example]]\nname = \"vortex\"\n\n[[example]]\nname = \"heart\""
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Wayoung7\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n<br>\n<img src=\"https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/demo_0.gif\" alt=\"gif\" width=\"800\">\n<br>\n<br>\nFirework-rs\n<br>\n</h1>\n\n<p align=\"center\">\n<a href=\"https://crates.io/crates/firework-rs\"><img alt=\"crates.io\" src=\"https://img.shields.io/crates/v/firework-rs.svg\"></a>\n<a><img alt=\"License\" src=\"https://img.shields.io/badge/License-MIT-blue.svg\"></a>\n</p>\n\nFirework-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!\n\n## Features\n\n - Colorful ASCII art firework\n - Smooth animation\n - Customizable fireworks\n - Simple particle system letting you make fireworks but not only fireworks\n\n## Try Out a Demo\n\nInstall [rust](https://www.rust-lang.org/tools/install) if you havn't.\n\nThen, simply run the following commands:\n\n```\ngit clone https://github.com/Wayoung7/firework-rs.git\ncd firework-rs\ncargo run --release -- -d 0\n```\n\nor to install globally on your computer:\n\n```\ncargo install firework-rs\nfirework -d 0\n```\n\nThe binary now has **5 demos**, from **0** to **4**. \n\n## Exit\n\nTo exit the program, simply press `ESC`\n\n## Command Line Arguments\n\n```\nUSAGE:\nfirework [OPTIONS] --demo <DEMO-NUMBER>\n\nOptions:\n    -d, --demo <DEMO-NUMBER>\n            Select which demo to run. (optional)\n          \n            If this is not specified, automatically run the infinite random firework demo\n\n    -l, --looping\n            Set whether the fireworks show will loop infinitely\n\n    -g, --gradient\n            Set whether the fireworks will have color gradient\n          \n            If this is enabled, it is recommanded that your terminal is non-transparent and has black bg color to get better visual effects\n\n        --fps <FRAME-RATE>\n            Set frame per second\n          \n            If this is not specified, the default fps is 12\n\n        --cjk\n          Set whether to enable cjk character\n          \n          If enabled, each character will take up two Latin character space\n\n    -h, --help\n            Print help (see a summary with '-h')\n\n    -V, --version\n            Print version\n```\n\n### Example Commands\n\nIf you have installed the binary:\n\nInfinite firework show with gradient enabled:\n\n```\nfirework -g\n```\n\nDemo 1 with looping and gradient enabled:\n\n```\nfirework -l -g -d 1\n```\n\nIf you have not installed the binary:\n\nFirst `cd` into the project root directory, and then run:\n\n```\ncargo run --release -- -g\n```\n\n```\ncargo run --release -- -l -g -d 1\n```\n\n## Use the Library\n\nThis 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.\n\nTo add this crate to your rust project, run:\n\n```\ncargo add firework_rs\n```\n\nin your project root directory.\n\nTo make a firework, you can simply use the following structure:\n\n```\nfn main() -> Result<()> {\n    // Terminal stuff, no need to change\n    let mut stdout = stdout();\n    let (_width, _height) = terminal::size()?;\n    let mut is_running = true;\n\n    terminal::enable_raw_mode()?;\n    execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;\n\n    let mut time = SystemTime::now();\n    let mut term = Terminal::default();\n\n    // Init and add fireworks\n    let mut fm = FireworkManager::default().add_firework(gen());\n\n    // Main loop, no need to change\n    while is_running {\n        if event::poll(Duration::ZERO)? {\n            match event::read()? {\n                event::Event::Key(e) => {\n                    if e.code == KeyCode::Esc {\n                        is_running = false;\n                    }\n                }\n                event::Event::Resize(_, _) => {\n                    fm.reset();\n                    term.reinit();\n                }\n                _ => {}\n            };\n        }\n\n        let delta_time = SystemTime::now().duration_since(time).unwrap();\n        fm.update(time, delta_time);\n        time = SystemTime::now();\n\n        term.render(&fm);\n        term.print(&mut stdout);\n\n        if delta_time < Duration::from_secs_f32(0.05) {\n            let rem = Duration::from_secs_f32(0.05) - delta_time;\n            sleep(rem);\n        }\n    }\n\n    execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?;\n    terminal::disable_raw_mode()?;\n\n    Ok(())\n}\n\n// Your actuall firework design goes here, see docs for more information\nfn gen() -> Firework {\n    let colors = vec![\n        ...\n    ];\n    let particles = ...\n    let config = ...\n\n    Firework {\n        ...\n    }\n}\n```\n\n### Examples\n\nThe package provide several examples under `examples/` showing some features of the library, and give you some inspiration.\n\nTo run examples, `cd` into the this project directory, and simply type:\n\n```\ncargo run --example <EXAMPLE-NAME>\n```\n\n**Example-name** contains:\n\nfountain\n\n<h4 align=\"center\">\n<img src=\"https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/fountain.gif\" alt=\"gif\" width=\"600\">\n</h4>\n\nvortex\n\n<h4 align=\"center\">\n<img src=\"https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/vortex.gif\" alt=\"gif\" width=\"600\">\n</h4>\n\nheart\n\n<h4 align=\"center\">\n<img src=\"https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/heart.gif\" alt=\"gif\" width=\"600\">\n</h4>\n\n## Compatibility\n\n### Operating System\n\nThis program can be run on Windows / Mac OS / Linux.\n\n### Terminal\nThis crate uses [crossterm](https://github.com/crossterm-rs/crossterm) as backend. Terminals that crossterm supports will also be supported by this crate.\n\nThis 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. \n\nIt 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**.\n\n## Help\n\nFeel free to open an issue or contact me if you find any bugs.\n\n\n\n\n"
  },
  {
    "path": "examples/fountain.rs",
    "content": "use std::{\n    f32::consts::PI,\n    io::{stdout, Result},\n    thread::sleep,\n    time::{Duration, SystemTime},\n};\n\nuse crossterm::{\n    cursor,\n    event::{self, KeyCode},\n    execute, terminal,\n};\nuse firework_rs::{\n    config::Config,\n    fireworks::{ExplosionForm, Firework, FireworkConfig, FireworkManager},\n    particle::ParticleConfig,\n    term::Terminal,\n    utils::gen_points_fan,\n};\nuse glam::Vec2;\nuse rand::{seq::IteratorRandom, thread_rng, Rng};\n\nfn main() -> Result<()> {\n    let mut stdout = stdout();\n    let (_width, _height) = terminal::size()?;\n    let mut is_running = true;\n    let cfg = Config::default();\n\n    terminal::enable_raw_mode()?;\n    execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;\n\n    let mut time = SystemTime::now();\n    let mut term = Terminal::default();\n    let mut fm = FireworkManager::default().with_firework(gen_fountain_firework(Vec2::new(\n        _width as f32 / 4.,\n        _height as f32 / 2. + 13.,\n    )));\n\n    while is_running {\n        if event::poll(Duration::ZERO)? {\n            match event::read()? {\n                event::Event::Key(e) => {\n                    if e.code == KeyCode::Esc {\n                        is_running = false;\n                    }\n                }\n                event::Event::Resize(_, _) => {\n                    fm.reset();\n                    term.reinit(&cfg);\n                }\n                _ => {}\n            };\n        }\n\n        let delta_time = SystemTime::now().duration_since(time).unwrap();\n        fm.update(time, delta_time);\n        time = SystemTime::now();\n\n        term.render(&fm, &cfg);\n        term.print(&mut stdout, &cfg);\n\n        if delta_time < Duration::from_secs_f32(0.05) {\n            let rem = Duration::from_secs_f32(0.05) - delta_time;\n            sleep(rem);\n        }\n    }\n\n    execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?;\n    terminal::disable_raw_mode()?;\n\n    Ok(())\n}\n\nfn gen_fountain_firework(center: Vec2) -> Firework {\n    let colors = vec![(226, 196, 136), (255, 245, 253), (208, 58, 99)];\n    let mut particles = Vec::new();\n    for v in gen_points_fan(\n        300.,\n        45,\n        5 as f32 / 12 as f32 * PI,\n        7 as f32 / 12 as f32 * PI,\n    )\n    .iter()\n    {\n        particles.push(ParticleConfig::new(\n            center,\n            *v,\n            thread_rng().gen_range(28..38),\n            Duration::from_secs_f32(thread_rng().gen_range(2.5..3.8)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default()\n        .with_ar_scale(0.15)\n        .with_gravity_scale(0.5)\n        .with_gradient_scale(gradient);\n    config.set_enable_gradient(true);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after: Duration::ZERO,\n        center,\n        particles,\n        config,\n        form: ExplosionForm::Sustained {\n            lasts: Duration::from_secs(5),\n            time_interval: Duration::from_secs_f32(0.08),\n            timer: Duration::ZERO,\n        },\n        ..Default::default()\n    }\n}\n\nfn gradient(x: f32) -> f32 {\n    if x < 0.8125 {\n        -0.4 * x + 1.1\n    } else {\n        -2. * x + 2.4\n    }\n}\n"
  },
  {
    "path": "examples/heart.rs",
    "content": "use std::{\n    f32::consts::PI,\n    io::{stdout, Result},\n    thread::sleep,\n    time::{Duration, SystemTime},\n};\n\nuse crossterm::{\n    cursor,\n    event::{self, KeyCode},\n    execute, terminal,\n};\nuse firework_rs::{\n    config::Config,\n    fireworks::{ExplosionForm, Firework, FireworkConfig, FireworkManager},\n    particle::ParticleConfig,\n    term::Terminal,\n    utils::gen_points_fan,\n};\nuse glam::Vec2;\nuse rand::{seq::IteratorRandom, thread_rng, Rng};\n\nfn main() -> Result<()> {\n    let mut stdout = stdout();\n    let (_width, _height) = terminal::size()?;\n    let mut is_running = true;\n    let cfg = Config::default();\n\n    terminal::enable_raw_mode()?;\n    execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;\n\n    let mut time = SystemTime::now();\n    let mut term = Terminal::default();\n    let mut fm = FireworkManager::default().with_firework(gen_heart_firework(Vec2::new(\n        _width as f32 / 4.,\n        _height as f32 / 2.,\n    )));\n\n    while is_running {\n        if event::poll(Duration::ZERO)? {\n            match event::read()? {\n                event::Event::Key(e) => {\n                    if e.code == KeyCode::Esc {\n                        is_running = false;\n                    }\n                }\n                event::Event::Resize(_, _) => {\n                    fm.reset();\n                    term.reinit(&cfg);\n                }\n                _ => {}\n            };\n        }\n\n        let delta_time = SystemTime::now().duration_since(time).unwrap();\n        fm.update(time, delta_time);\n        time = SystemTime::now();\n\n        term.render(&fm, &cfg);\n        term.print(&mut stdout, &cfg);\n\n        if delta_time < Duration::from_secs_f32(0.05) {\n            let rem = Duration::from_secs_f32(0.05) - delta_time;\n            sleep(rem);\n        }\n    }\n\n    execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?;\n    terminal::disable_raw_mode()?;\n\n    Ok(())\n}\n\nfn gen_heart_firework(center: Vec2) -> Firework {\n    let colors = vec![\n        (233, 232, 237),\n        (254, 142, 130),\n        (200, 27, 72),\n        (86, 18, 31),\n    ];\n    let mut particles = Vec::new();\n    let trail_length = thread_rng().gen_range(100..105);\n    let life_time = Duration::from_secs_f32(thread_rng().gen_range(3.0..3.2));\n    let init_pos = center - Vec2::NEG_Y * 15.;\n    for v in gen_points_fan(300., 45, 0.2 * PI, 0.3 * PI).iter() {\n        particles.push(ParticleConfig::new(\n            init_pos,\n            *v,\n            trail_length,\n            life_time,\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    for v in gen_points_fan(300., 45, 0.7 * PI, 0.8 * PI).iter() {\n        particles.push(ParticleConfig::new(\n            init_pos,\n            *v,\n            trail_length,\n            life_time,\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default()\n        .with_ar_scale(0.1)\n        .with_gravity_scale(0.1)\n        .with_gradient_scale(gradient)\n        .with_additional_force(move |particle| (center - particle.pos) * 2.);\n    config.set_enable_gradient(true);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after: Duration::ZERO,\n        center,\n        particles,\n        config,\n        form: ExplosionForm::Instant { used: false },\n        ..Default::default()\n    }\n}\n\nfn gradient(x: f32) -> f32 {\n    if x < 0.8125 {\n        -0.4 * x + 1.1\n    } else {\n        -2. * x + 2.2\n    }\n}\n"
  },
  {
    "path": "examples/vortex.rs",
    "content": "use std::{\n    io::{stdout, Result},\n    thread::sleep,\n    time::{Duration, SystemTime},\n};\n\nuse crossterm::{\n    cursor,\n    event::{self, KeyCode},\n    execute, terminal,\n};\nuse firework_rs::{\n    config::Config,\n    fireworks::{ExplosionForm, Firework, FireworkConfig, FireworkManager},\n    particle::ParticleConfig,\n    term::Terminal,\n    utils::gen_points_circle,\n};\nuse glam::Vec2;\nuse rand::{seq::IteratorRandom, thread_rng, Rng};\n\nfn main() -> Result<()> {\n    let mut stdout = stdout();\n    let (_width, _height) = terminal::size()?;\n    let mut is_running = true;\n    let cfg = Config::default();\n\n    terminal::enable_raw_mode()?;\n    execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;\n\n    let mut time = SystemTime::now();\n    let mut term = Terminal::default();\n    let mut fm = FireworkManager::default().with_firework(gen_vortex_firework(Vec2::new(\n        _width as f32 / 4.,\n        _height as f32 / 2.,\n    )));\n\n    while is_running {\n        if event::poll(Duration::ZERO)? {\n            match event::read()? {\n                event::Event::Key(e) => {\n                    if e.code == KeyCode::Esc {\n                        is_running = false;\n                    }\n                }\n                event::Event::Resize(_, _) => {\n                    fm.reset();\n                    term.reinit(&cfg);\n                }\n                _ => {}\n            };\n        }\n\n        let delta_time = SystemTime::now().duration_since(time).unwrap();\n        fm.update(time, delta_time);\n        time = SystemTime::now();\n\n        term.render(&fm, &cfg);\n        term.print(&mut stdout, &cfg);\n\n        if delta_time < Duration::from_secs_f32(0.05) {\n            let rem = Duration::from_secs_f32(0.05) - delta_time;\n            sleep(rem);\n        }\n    }\n\n    execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?;\n    terminal::disable_raw_mode()?;\n\n    Ok(())\n}\n\nfn gen_vortex_firework(center: Vec2) -> Firework {\n    let colors = vec![\n        (6, 55, 63),\n        (24, 90, 96),\n        (47, 123, 119),\n        (92, 174, 166),\n        (200, 255, 255),\n    ];\n    let mut particles = Vec::new();\n    for p in gen_points_circle(30, 45).iter() {\n        particles.push(ParticleConfig::new(\n            center + *p,\n            Vec2::new((*p).y, -(*p).x).normalize() * 15.,\n            thread_rng().gen_range(28..40),\n            Duration::from_secs_f32(thread_rng().gen_range(4.5..7.0)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default()\n        .with_ar_scale(0.05)\n        .with_gravity_scale(0.)\n        .with_gradient_scale(gradient)\n        .with_additional_force(move |particle| {\n            (center - particle.pos).normalize() * (1. / center.distance(particle.pos)) * 150.\n        });\n    config.set_enable_gradient(true);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after: Duration::ZERO,\n        center,\n        particles,\n        config,\n        form: ExplosionForm::Sustained {\n            lasts: Duration::from_secs(10),\n            time_interval: Duration::from_secs_f32(0.01),\n            timer: Duration::ZERO,\n        },\n        ..Default::default()\n    }\n}\n\nfn gradient(x: f32) -> f32 {\n    if x < 0.8125 {\n        -0.4 * x + 1.1\n    } else {\n        -2. * x + 2.2\n    }\n}\n"
  },
  {
    "path": "src/bin/firework/args.rs",
    "content": "use clap::Parser;\n\n/// Used to receive command line arguments\n#[derive(Parser)]\n#[command(version, about, long_about = None)]\npub struct Cli {\n    /// Select which demo to run. (optional)\n    ///\n    /// If this is not specified, automatically run the infinite random firework demo\n    #[arg(short, long, value_name = \"DEMO-NUMBER\")]\n    pub demo: Option<u8>,\n\n    /// Set whether the fireworks show will loop infinitely\n    #[arg(short, long)]\n    pub looping: bool,\n\n    /// Set whether the fireworks will have color gradient\n    ///\n    /// If this is enabled, it is recommanded that your terminal is non-transparent and has black bg color to get better visual effects\n    #[arg(short, long)]\n    pub gradient: bool,\n\n    /// Set frame per second\n    ///\n    /// If this is not specified, the default fps is 12\n    #[arg(long, value_name = \"FRAME-RATE\")]\n    pub fps: Option<u8>,\n\n    /// Set whether to enable cjk character\n    ///\n    /// If enabled, each character will take up two Latin character space\n    #[arg(long)]\n    pub cjk: bool,\n}\n"
  },
  {
    "path": "src/bin/firework/gen.rs",
    "content": "use std::time::Duration;\n\nuse firework_rs::{config::Config, demo::demo_firework_0, fireworks::FireworkManager};\nuse glam::Vec2;\nuse rand::{seq::IteratorRandom, thread_rng, Rng};\n\npub fn dyn_gen(\n    fm: &mut FireworkManager,\n    width: u16,\n    height: u16,\n    enable_gradient: bool,\n    cfg: &Config,\n) {\n    let colors = [\n        vec![\n            (255, 102, 75),\n            (144, 56, 67),\n            (255, 225, 124),\n            (206, 32, 41),\n        ],\n        vec![\n            (235, 39, 155),\n            (250, 216, 68),\n            (242, 52, 72),\n            (63, 52, 200),\n            (255, 139, 57),\n        ],\n        vec![\n            (152, 186, 227),\n            (89, 129, 177),\n            (54, 84, 117),\n            (240, 244, 254),\n        ],\n        vec![\n            (34, 87, 122),\n            (56, 163, 165),\n            (87, 204, 153),\n            (128, 237, 153),\n            (199, 249, 204),\n        ],\n        vec![\n            (205, 180, 219),\n            (255, 200, 221),\n            (255, 175, 204),\n            (189, 224, 254),\n            (162, 210, 255),\n        ],\n        vec![\n            (79, 0, 11),\n            (114, 0, 38),\n            (206, 66, 87),\n            (255, 127, 81),\n            (255, 155, 84),\n        ],\n        vec![(0, 29, 61), (0, 53, 102), (255, 195, 0), (255, 214, 10)],\n        vec![\n            (61, 52, 139),\n            (118, 120, 237),\n            (247, 184, 1),\n            (241, 135, 1),\n            (243, 91, 4),\n        ],\n    ];\n    let limit = if cfg.enable_cjk {\n        (width as usize * height as usize) / 1800 + 3\n    } else {\n        (width as usize * height as usize) / 1300 + 3\n    };\n    if fm.fireworks.len() < limit {\n        let x: isize = thread_rng().gen_range(-3..(width as isize + 3));\n        let y: isize = thread_rng().gen_range(-1..(height as isize + 1));\n        fm.add_firework(demo_firework_0(\n            Vec2::new(x as f32, y as f32),\n            Duration::from_secs_f32(thread_rng().gen_range(0.0..2.0)),\n            enable_gradient,\n            colors.iter().choose(&mut thread_rng()).unwrap().to_owned(),\n            cfg,\n        ));\n    }\n}\n"
  },
  {
    "path": "src/bin/firework/main.rs",
    "content": "//! With the `firework` binary, you can run some pre-designed fireworks with command line arguments\n\nmod args;\nmod gen;\n\nuse std::{\n    io::{stdout, Error, Result},\n    thread::sleep,\n    time::{Duration, SystemTime},\n};\n\nuse args::Cli;\nuse clap::Parser;\nuse crossterm::{\n    cursor,\n    event::{self, KeyCode},\n    execute, terminal,\n};\nuse firework_rs::term::Terminal;\nuse firework_rs::{config::Config, fireworks::FireworkManager};\nuse firework_rs::{\n    demo::{\n        demo_firework_2, demo_firework_comb_0, demo_firework_comb_1, demo_firework_comb_2,\n        demo_firework_comb_3,\n    },\n    fireworks::FireworkInstallForm,\n};\nuse gen::dyn_gen;\nuse glam::Vec2;\n\nfn main() -> Result<()> {\n    let mut cfg = Config::default();\n    let mut fps: u8 = 20;\n    let mut is_running = true;\n    let cli = Cli::parse();\n    if cli.cjk {\n        cfg = Config { enable_cjk: true };\n    }\n    if let Some(f) = cli.fps {\n        if !(5..=30).contains(&f) {\n            return Err(Error::new(\n                std::io::ErrorKind::Other,\n                \"Invalid fps value! Valid fps range: 5~30\",\n            ));\n        } else {\n            fps = f;\n        }\n    }\n    let (mut _width, mut _height) = terminal::size()?;\n    let mut fm = match cli.demo {\n        Some(0) => FireworkManager::default().with_fireworks(demo_firework_comb_0(\n            Vec2::new(_width as f32 / 4., _height as f32 / 2.),\n            Duration::from_secs_f32(0.7),\n            cli.gradient,\n        )),\n        Some(1) => FireworkManager::default().with_fireworks(demo_firework_comb_2(\n            Vec2::new(_width as f32 / 4., _height as f32 / 2.),\n            Duration::from_secs_f32(0.7),\n            cli.gradient,\n        )),\n        Some(2) => FireworkManager::default().with_fireworks(demo_firework_comb_3(\n            Vec2::new(_width as f32 / 4., _height as f32 / 2.),\n            Duration::from_secs_f32(0.7),\n            cli.gradient,\n        )),\n        Some(3) => FireworkManager::default().with_fireworks(demo_firework_comb_1(\n            Vec2::new(_width as f32 / 4., 66.),\n            Duration::from_secs_f32(0.2),\n            cli.gradient,\n        )),\n        Some(4) => FireworkManager::default().with_firework(demo_firework_2(\n            Vec2::new(_width as f32 / 4., _height as f32 / 2.),\n            Duration::from_secs_f32(0.7),\n            cli.gradient,\n        )),\n        None => FireworkManager::default().enable_dyn_install(),\n        _ => {\n            return Err(Error::new(\n                std::io::ErrorKind::Other,\n                \"Invalid demo number! Demo number should be: 0~4\",\n            ));\n        }\n    };\n    fm.set_enable_loop(cli.looping);\n\n    let mut stdout = stdout();\n    terminal::enable_raw_mode()?;\n    execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;\n\n    let mut time = SystemTime::now();\n    let mut term = Terminal::new(&cfg);\n\n    while is_running {\n        if event::poll(Duration::ZERO)? {\n            match event::read()? {\n                event::Event::Key(e) => {\n                    if e.code == KeyCode::Esc {\n                        is_running = false;\n                    }\n                }\n                event::Event::Resize(_, _) => {\n                    fm.reset();\n                    term.reinit(&cfg);\n                }\n                _ => {}\n            };\n        }\n\n        (_width, _height) = terminal::size()?;\n        let delta_time = SystemTime::now().duration_since(time).unwrap();\n        if fm.install_form == FireworkInstallForm::DynamicInstall {\n            dyn_gen(\n                &mut fm,\n                if cfg.enable_cjk {\n                    (_width - 1) / 2\n                } else {\n                    _width\n                },\n                _height,\n                cli.gradient,\n                &cfg,\n            );\n        }\n        fm.update(time, delta_time);\n        time = SystemTime::now();\n        term.render(&fm, &cfg);\n        term.print(&mut stdout, &cfg);\n\n        if delta_time < Duration::from_secs_f32(1. / fps as f32) {\n            let rem = Duration::from_secs_f32(1. / fps as f32) - delta_time;\n            sleep(rem);\n        }\n    }\n\n    execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen)?;\n    terminal::disable_raw_mode()?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "/// Configuration of the program\n#[derive(Default)]\npub struct Config {\n    pub enable_cjk: bool,\n}\n"
  },
  {
    "path": "src/demo.rs",
    "content": "//! This module provides some demos of different types of fireworks\n\nuse std::{\n    f32::consts::PI,\n    time::{Duration, SystemTime},\n};\n\nuse glam::Vec2;\nuse rand::{seq::IteratorRandom, thread_rng, Rng};\n\nuse crate::{\n    config::Config,\n    fireworks::{ExplosionForm, Firework, FireworkConfig},\n    particle::ParticleConfig,\n    utils::{\n        explosion_gradient_1, explosion_gradient_2, explosion_gradient_3, gen_points_arc,\n        gen_points_circle, gen_points_circle_normal, gen_points_circle_normal_dev, gen_points_fan,\n        linear_gradient_1,\n    },\n};\n\npub fn demo_firework_0(\n    center: Vec2,\n    spawn_after: Duration,\n    enable_gradient: bool,\n    colors: Vec<(u8, u8, u8)>,\n    cfg: &Config,\n) -> Firework {\n    let mut particles = Vec::new();\n    for v in gen_points_circle_normal(\n        thread_rng().gen_range(if cfg.enable_cjk {\n            400.0..600.0\n        } else {\n            230.0..400.0\n        }),\n        thread_rng().gen_range(if cfg.enable_cjk { 20..35 } else { 33..47 }),\n    )\n    .iter()\n    {\n        particles.push(ParticleConfig::new(\n            center,\n            *v,\n            thread_rng().gen_range(20..25),\n            Duration::from_secs_f32(thread_rng().gen_range(1.8..2.3)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default().with_gradient_scale(explosion_gradient_1);\n    config.set_enable_gradient(enable_gradient);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after,\n        center,\n        particles,\n        config,\n        ..Default::default()\n    }\n}\n\npub fn demo_firework_1(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework {\n    let colors = [\n        (255, 102, 75),\n        (144, 56, 67),\n        (255, 225, 124),\n        (206, 32, 41),\n    ];\n    let mut particles = Vec::new();\n    for v in gen_points_circle_normal(250., 45).iter() {\n        particles.push(ParticleConfig::new(\n            center,\n            *v,\n            thread_rng().gen_range(23..27),\n            Duration::from_secs_f32(thread_rng().gen_range(2.1..2.7)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default().with_gradient_scale(explosion_gradient_1);\n    config.set_enable_gradient(enable_gradient);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after,\n        center,\n        particles,\n        config,\n        ..Default::default()\n    }\n}\n\npub fn demo_firework_2(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework {\n    let colors = [(250, 216, 68)];\n    let mut particles = Vec::new();\n    for v in gen_points_circle(100, 600).iter() {\n        particles.push(ParticleConfig::new(\n            center,\n            *v,\n            thread_rng().gen_range(5..8),\n            Duration::from_secs_f32(thread_rng().gen_range(3.0..5.5)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default()\n        .with_gradient_scale(explosion_gradient_2)\n        .with_gravity_scale(0.)\n        .with_ar_scale(0.15);\n    config.set_enable_gradient(enable_gradient);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after,\n        center,\n        particles,\n        config,\n        ..Default::default()\n    }\n}\n\npub fn demo_firework_3(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework {\n    let colors = [\n        (242, 233, 190),\n        (226, 196, 136),\n        (149, 202, 176),\n        (26, 64, 126),\n    ];\n    let mut particles = Vec::new();\n    for v in gen_points_circle_normal(350., 135).iter() {\n        particles.push(ParticleConfig::new(\n            center,\n            *v,\n            thread_rng().gen_range(23..43),\n            Duration::from_secs_f32(thread_rng().gen_range(3.5..5.0)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default()\n        .with_gradient_scale(explosion_gradient_1)\n        .with_ar_scale(0.18)\n        .with_gravity_scale(0.7);\n    config.set_enable_gradient(enable_gradient);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after,\n        center,\n        particles,\n        config,\n        ..Default::default()\n    }\n}\n\npub fn demo_firework_4(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework {\n    let colors = [(242, 233, 190), (226, 196, 136), (255, 248, 253)];\n    let mut particles = Vec::new();\n    for v in gen_points_circle_normal(350., 25).iter() {\n        particles.push(ParticleConfig::new(\n            center,\n            *v,\n            thread_rng().gen_range(20..33),\n            Duration::from_secs_f32(thread_rng().gen_range(3.5..5.0)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default()\n        .with_gradient_scale(explosion_gradient_1)\n        .with_gravity_scale(0.3);\n    config.set_enable_gradient(enable_gradient);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after,\n        center,\n        particles,\n        config,\n        ..Default::default()\n    }\n}\n\npub fn demo_firework_5(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework {\n    let colors = [(152, 186, 227), (54, 84, 117), (21, 39, 60)];\n    let mut particles = Vec::new();\n    for v in gen_points_circle_normal(450., 80).iter() {\n        particles.push(ParticleConfig::new(\n            center,\n            *v,\n            thread_rng().gen_range(33..43),\n            Duration::from_secs_f32(thread_rng().gen_range(3.5..5.0)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default()\n        .with_gradient_scale(explosion_gradient_3)\n        .with_gravity_scale(1.4);\n    config.set_enable_gradient(enable_gradient);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after,\n        center,\n        particles,\n        config,\n        ..Default::default()\n    }\n}\n\npub fn demo_firework_6(center: Vec2, spawn_after: Duration, enable_gradient: bool) -> Firework {\n    let colors = [(242, 233, 190), (226, 196, 136), (255, 248, 253)];\n    let mut particles = Vec::new();\n    for v in gen_points_circle_normal(350., 35).iter() {\n        particles.push(ParticleConfig::new(\n            center,\n            *v,\n            thread_rng().gen_range(20..23),\n            Duration::from_secs_f32(thread_rng().gen_range(3.5..4.0)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config = FireworkConfig::default()\n        .with_gradient_scale(explosion_gradient_1)\n        .with_ar_scale(0.19)\n        .with_gravity_scale(0.1);\n    config.set_enable_gradient(enable_gradient);\n    Firework {\n        init_time: SystemTime::now(),\n        spawn_after,\n        center,\n        particles,\n        config,\n        ..Default::default()\n    }\n}\n\npub fn demo_firework_comb_1(\n    start: Vec2,\n    spawn_after: Duration,\n    enable_gradient: bool,\n) -> Vec<Firework> {\n    // Ascent of rocket\n    let color1 = (255, 255, 235);\n    let particles1 = ParticleConfig::new(\n        start,\n        Vec2::NEG_Y * 160.,\n        6,\n        Duration::from_secs_f32(1.2),\n        color1,\n    );\n    let mut config1 = FireworkConfig::default()\n        .with_ar_scale(0.04)\n        .with_gradient_scale(linear_gradient_1);\n    config1.set_enable_gradient(enable_gradient);\n\n    // Explosion\n    let color2 = [\n        (235, 39, 155),\n        (250, 216, 68),\n        (242, 52, 72),\n        (63, 52, 200),\n        (255, 139, 57),\n    ];\n    let center2 = start + Vec2::NEG_Y * 53.;\n    let mut particles2 = Vec::new();\n    for v in gen_points_circle_normal(350., 160).iter() {\n        particles2.push(ParticleConfig::new(\n            center2,\n            *v,\n            thread_rng().gen_range(23..43),\n            Duration::from_secs_f32(thread_rng().gen_range(2.5..4.5)),\n            *color2.iter().choose(&mut thread_rng()).unwrap(),\n        ));\n    }\n    let mut config2 = FireworkConfig::default()\n        .with_gradient_scale(explosion_gradient_1)\n        .with_ar_scale(0.2)\n        .with_gravity_scale(0.3);\n    config2.set_enable_gradient(enable_gradient);\n    vec![\n        Firework {\n            init_time: SystemTime::now(),\n            spawn_after,\n            center: start,\n            particles: vec![particles1],\n            config: config1,\n            ..Default::default()\n        },\n        Firework {\n            init_time: SystemTime::now(),\n            spawn_after: spawn_after + Duration::from_secs_f32(1.2),\n            center: center2,\n            particles: particles2,\n            config: config2,\n            ..Default::default()\n        },\n    ]\n}\n\npub fn demo_firework_comb_0(\n    center: Vec2,\n    spawn_after: Duration,\n    enable_gradient: bool,\n) -> Vec<Firework> {\n    vec![\n        demo_firework_3(center + Vec2::new(-5., -19.), spawn_after, enable_gradient),\n        demo_firework_4(\n            center + Vec2::new(-30., 0.),\n            spawn_after + Duration::from_secs_f32(0.4),\n            enable_gradient,\n        ),\n        demo_firework_5(\n            center + Vec2::new(12., 0.),\n            spawn_after + Duration::from_secs_f32(1.6),\n            enable_gradient,\n        ),\n        demo_firework_1(\n            center + Vec2::new(-9., 7.),\n            spawn_after + Duration::from_secs_f32(2.),\n            enable_gradient,\n        ),\n        demo_firework_6(\n            center + Vec2::new(24., -11.),\n            spawn_after + Duration::from_secs_f32(2.3),\n            enable_gradient,\n        ),\n    ]\n}\n\npub fn demo_firework_comb_2(\n    center: Vec2,\n    spawn_after: Duration,\n    enable_gradient: bool,\n) -> Vec<Firework> {\n    let mut res = Vec::new();\n    let fountain1 = |center: Vec2, angle: f32| {\n        let colors = [(255, 183, 3), (251, 133, 0), (242, 233, 190)];\n        let mut particles = Vec::new();\n        for v in gen_points_fan(60., 20, angle - 0.05, angle + 0.05).iter() {\n            particles.push(ParticleConfig::new(\n                center,\n                *v,\n                thread_rng().gen_range(28..38),\n                Duration::from_secs_f32(thread_rng().gen_range(2.5..3.8)),\n                *colors.iter().choose(&mut thread_rng()).unwrap(),\n            ));\n        }\n        let mut config = FireworkConfig::default()\n            .with_ar_scale(0.05)\n            .with_gradient_scale(linear_gradient_1);\n        config.set_enable_gradient(enable_gradient);\n        Firework {\n            init_time: SystemTime::now(),\n            spawn_after,\n            center,\n            particles,\n            config,\n            form: ExplosionForm::Sustained {\n                lasts: Duration::from_secs(5),\n                time_interval: Duration::from_secs_f32(0.08),\n                timer: Duration::ZERO,\n            },\n            ..Default::default()\n        }\n    };\n\n    let fountain2 = |center: Vec2| {\n        let colors = [(226, 196, 136), (255, 245, 253), (208, 58, 99)];\n        let mut particles = Vec::new();\n        for v in gen_points_fan(1000., 20, 5.7 / 12. * PI, 6.3 / 12. * PI).iter() {\n            particles.push(ParticleConfig::new(\n                center,\n                *v,\n                thread_rng().gen_range(28..38),\n                Duration::from_secs_f32(thread_rng().gen_range(2.5..3.8)),\n                *colors.iter().choose(&mut thread_rng()).unwrap(),\n            ));\n        }\n        let mut config = FireworkConfig::default()\n            .with_ar_scale(0.14)\n            .with_gravity_scale(0.9)\n            .with_gradient_scale(linear_gradient_1);\n        config.set_enable_gradient(enable_gradient);\n        Firework {\n            init_time: SystemTime::now(),\n            spawn_after: spawn_after + Duration::from_secs_f32(4.),\n            center,\n            particles,\n            config,\n            form: ExplosionForm::Sustained {\n                lasts: Duration::from_secs(5),\n                time_interval: Duration::from_secs_f32(0.08),\n                timer: Duration::ZERO,\n            },\n            ..Default::default()\n        }\n    };\n\n    let mono = |center: Vec2, sa: Duration, colors: Vec<(u8, u8, u8)>| {\n        let particles = vec![ParticleConfig::new(\n            center,\n            gen_points_arc(200., 1, 5. / 12. * PI, 7. / 12. * PI)[0],\n            thread_rng().gen_range(24..30),\n            Duration::from_secs_f32(thread_rng().gen_range(2.1..2.7)),\n            *colors.iter().choose(&mut thread_rng()).unwrap(),\n        )];\n        let mut config = FireworkConfig::default()\n            .with_ar_scale(thread_rng().gen_range(0.18..0.24))\n            .with_gradient_scale(linear_gradient_1);\n        config.set_enable_gradient(enable_gradient);\n        Firework {\n            init_time: SystemTime::now(),\n            spawn_after: sa,\n            center,\n            particles,\n            config,\n            form: ExplosionForm::Instant { used: false },\n            ..Default::default()\n        }\n    };\n\n    res.push(fountain1(center + Vec2::new(-31., 21.), 1.05));\n    res.push(fountain1(center + Vec2::new(31., 21.), 2.09));\n\n    res.push(fountain2(center + Vec2::new(-7., 21.)));\n    res.push(fountain2(center + Vec2::new(7., 21.)));\n\n    (-33..=33).step_by(3).for_each(|i| {\n        res.push(mono(\n            Vec2::new(center.x + i as f32, center.y + 21.),\n            Duration::from_secs_f32(3.5),\n            vec![(0, 119, 182), (144, 224, 239), (12, 180, 216)],\n        ))\n    });\n\n    (-33..=33).step_by(3).for_each(|i| {\n        res.push(mono(\n            Vec2::new(center.x + i as f32, center.y + 21.),\n            Duration::from_secs_f32(4.7),\n            vec![(181, 23, 158), (247, 37, 133), (114, 9, 183)],\n        ))\n    });\n\n    (-33..=33).step_by(3).for_each(|i| {\n        res.push(mono(\n            Vec2::new(center.x + i as f32, center.y + 21.),\n            Duration::from_secs_f32(5.9),\n            vec![(217, 237, 146), (153, 217, 140), (82, 182, 154)],\n        ))\n    });\n\n    res\n}\n\npub fn demo_firework_comb_3(\n    center: Vec2,\n    spawn_after: Duration,\n    enable_gradient: bool,\n) -> Vec<Firework> {\n    let mut res = Vec::new();\n    let f1 = {\n        let colors = [(255, 216, 190), (255, 238, 221), (248, 247, 255)];\n        let mut particles = Vec::new();\n        for v in gen_points_circle_normal_dev(14., 200, 60.).iter() {\n            particles.push(ParticleConfig::new(\n                center + Vec2::NEG_Y * 6.,\n                *v,\n                thread_rng().gen_range(15..20),\n                Duration::from_secs_f32(thread_rng().gen_range(3.0..5.0)),\n                *colors.iter().choose(&mut thread_rng()).unwrap(),\n            ));\n        }\n        let mut config = FireworkConfig::default()\n            .with_gradient_scale(explosion_gradient_1)\n            .with_ar_scale(0.15)\n            .with_gravity_scale(0.35);\n        config.set_enable_gradient(enable_gradient);\n        Firework {\n            init_time: SystemTime::now(),\n            spawn_after,\n            center,\n            particles,\n            config,\n            ..Default::default()\n        }\n    };\n    res.push(f1);\n    let f2 = {\n        let colors = [\n            (152, 186, 227),\n            (89, 129, 177),\n            (54, 84, 117),\n            (240, 244, 254),\n        ];\n        let mut particles = Vec::new();\n        for v in gen_points_circle_normal_dev(10000., 600, 30.).iter() {\n            particles.push(ParticleConfig::new(\n                center + Vec2::NEG_Y * 6.,\n                *v,\n                thread_rng().gen_range(20..28),\n                Duration::from_secs_f32(thread_rng().gen_range(4.8..10.)),\n                *colors.iter().choose(&mut thread_rng()).unwrap(),\n            ));\n        }\n        let mut config = FireworkConfig::default()\n            .with_gradient_scale(explosion_gradient_1)\n            .with_ar_scale(0.09)\n            .with_gravity_scale(0.5);\n        config.set_enable_gradient(enable_gradient);\n        Firework {\n            init_time: SystemTime::now(),\n            spawn_after,\n            center,\n            particles,\n            config,\n            ..Default::default()\n        }\n    };\n    res.push(f2);\n\n    for (idx, p) in gen_points_circle(27, 10).iter().enumerate() {\n        let colors = [(17, 138, 178), (6, 214, 160), (7, 59, 76), (255, 255, 255)];\n        let mut particles = Vec::new();\n        for v in gen_points_circle_normal_dev(100., 35, 350. / 9.).iter() {\n            particles.push(ParticleConfig::new(\n                center + *p,\n                *v,\n                thread_rng().gen_range(20..30),\n                Duration::from_secs_f32(thread_rng().gen_range(3.0..4.0)),\n                *colors.iter().choose(&mut thread_rng()).unwrap(),\n            ));\n        }\n        let mut config = FireworkConfig::default()\n            .with_gradient_scale(explosion_gradient_3)\n            .with_ar_scale(0.28)\n            .with_gravity_scale(0.25);\n        config.set_enable_gradient(enable_gradient);\n        res.push(Firework {\n            init_time: SystemTime::now(),\n            spawn_after: spawn_after + Duration::from_secs_f32(0.2 * (idx + 4) as f32),\n            center,\n            particles,\n            config,\n            ..Default::default()\n        });\n    }\n\n    res\n}\n"
  },
  {
    "path": "src/fireworks.rs",
    "content": "//! `firework` module provides functions to define, create and update fireworks\n\nuse std::{\n    collections::VecDeque,\n    time::{Duration, SystemTime},\n};\n\nuse glam::Vec2;\nuse rand::{seq::IteratorRandom, thread_rng};\n\nuse crate::particle::{LifeState, Particle, ParticleConfig};\n\n/// Struct representing a single firework\npub struct Firework {\n    /// The `SystemTime` when the object is initialized/defined\n    pub init_time: SystemTime,\n    /// Firework spawns after `spawn_after` from `init_time`\n    pub spawn_after: Duration,\n    pub time_elapsed: Duration,\n    pub center: Vec2,\n    pub state: FireworkState,\n    pub config: FireworkConfig,\n    pub form: ExplosionForm,\n    pub particles: Vec<ParticleConfig>,\n    pub current_particles: Vec<Particle>,\n}\n\nimpl Default for Firework {\n    fn default() -> Self {\n        Self {\n            init_time: SystemTime::now(),\n            spawn_after: Duration::ZERO,\n            time_elapsed: Duration::ZERO,\n            center: Vec2::ZERO,\n            state: FireworkState::Waiting,\n            config: FireworkConfig::default(),\n            form: ExplosionForm::Instant { used: false },\n            particles: Vec::new(),\n            current_particles: Vec::new(),\n        }\n    }\n}\n\nimpl Firework {\n    /// Update the `Firework`\n    ///\n    /// # Arguments\n    ///\n    /// * `now` - `SystemTime` of now\n    /// * `delta_time` - `Duration` since last update\n    pub fn update(&mut self, now: SystemTime, delta_time: Duration) {\n        // Spawn particles\n        if now >= self.init_time + self.spawn_after {\n            self.time_elapsed += delta_time;\n            match &mut self.form {\n                ExplosionForm::Instant { used } => {\n                    if !*used {\n                        self.particles.iter().for_each(|p| {\n                            self.current_particles.push(Particle {\n                                pos: p.init_pos,\n                                vel: p.init_vel,\n                                trail: init_trail(p.init_pos, p.trail_length),\n                                life_state: LifeState::Alive,\n                                time_elapsed: Duration::ZERO,\n                                config: *p,\n                            })\n                        })\n                    }\n                    *used = true;\n                }\n                ExplosionForm::Sustained {\n                    lasts,\n                    time_interval,\n                    timer,\n                } => {\n                    if self.time_elapsed <= *lasts {\n                        if *timer + delta_time <= *time_interval {\n                            *timer += delta_time;\n                        } else {\n                            let n =\n                                (*timer + delta_time).as_millis() / (*time_interval).as_millis();\n                            self.particles\n                                .iter()\n                                .choose_multiple(&mut thread_rng(), n as usize)\n                                .iter()\n                                .for_each(|p| {\n                                    self.current_particles.push(Particle {\n                                        pos: p.init_pos,\n                                        vel: p.init_vel,\n                                        trail: init_trail(p.init_pos, p.trail_length),\n                                        life_state: LifeState::Alive,\n                                        time_elapsed: Duration::ZERO,\n                                        config: **p,\n                                    })\n                                });\n                            *timer = Duration::from_millis(\n                                ((*timer + delta_time).as_millis() % (*time_interval).as_millis())\n                                    as u64,\n                            );\n                        }\n                    }\n                }\n            }\n            self.state = FireworkState::Alive;\n        }\n\n        self.current_particles\n            .iter_mut()\n            .for_each(|p| p.update(delta_time, &self.config));\n\n        // Clean the dead pariticles\n        self.current_particles\n            .retain(|p| p.life_state != LifeState::Dead);\n\n        match self.form {\n            ExplosionForm::Instant { used } => {\n                if used && self.state == FireworkState::Alive && self.current_particles.is_empty() {\n                    self.state = FireworkState::Gone;\n                }\n            }\n            ExplosionForm::Sustained { lasts, .. } => {\n                if self.time_elapsed > lasts\n                    && self.state == FireworkState::Alive\n                    && self.current_particles.is_empty()\n                {\n                    self.state = FireworkState::Gone;\n                }\n            }\n        }\n    }\n\n    /// Return true if the `FireworkState` is `Gone`\n    pub fn is_gone(&self) -> bool {\n        self.state == FireworkState::Gone\n    }\n\n    /// Reset `Firework` to its initial state\n    pub fn reset(&mut self) {\n        self.init_time = SystemTime::now();\n        self.state = FireworkState::Waiting;\n        self.time_elapsed = Duration::ZERO;\n        self.current_particles = Vec::new();\n        match &mut self.form {\n            ExplosionForm::Instant { used } => {\n                *used = false;\n            }\n            ExplosionForm::Sustained { timer, .. } => {\n                *timer = Duration::ZERO;\n            }\n        }\n    }\n}\n\n/// Struct representing state of a `Firework`\n///\n/// State goes from `Waiting` -> `Alive` -> `Gone`\n///\n/// # Notes\n///\n/// - `Firework` turns to `Alive` when it is spawned\n/// - `Firework` turns to `Gone` when all of its `Particles` are `Dead`\n#[derive(Debug, PartialEq, Default)]\npub enum FireworkState {\n    #[default]\n    Waiting,\n    Alive,\n    Gone,\n}\n\n/// Enum that represents whether the `Firework` make one instantaneous explosion or continuously emit particles\n#[derive(Debug, PartialEq, Eq)]\npub enum ExplosionForm {\n    Instant {\n        used: bool,\n    },\n    Sustained {\n        /// `Duration` that the sustained firework will last\n        lasts: Duration,\n        /// Time interval between two particle spawn\n        time_interval: Duration,\n        timer: Duration,\n    },\n}\n\n/// Struct representing the configuration of a single `Firework`\n///\n/// This applies to all `Particle` in the `Firework`\npub struct FireworkConfig {\n    /// Larger `gravity_scale` tends to pull particles down\n    pub gravity_scale: f32,\n    /// Air resistance scale\n    /// Warning: too large or too small `ar_scale` may lead to unexpected behavior of `Particles`\n    pub ar_scale: f32,\n    pub additional_force: Box<dyn Fn(&Particle) -> Vec2>,\n    /// This field is a function that takes a float between 0 and 1, returns a float representing all `Particle`s' gradient\n    ///\n    /// `Particle`s' gradient changes according to its elapsed time and lifetime\n    /// The input `f32` equals to `time_elapsed`/`life_time`, which returns a `f32` affecting its color gradient\n    /// `gradient_scale` returns 1. means`Particle` will have the same colors as defined all over its lifetime\n    pub gradient_scale: fn(f32) -> f32,\n    /// Set wheter or not firework has color gradient\n    ///\n    /// # Notes\n    ///\n    /// - It is recommanded that your terminal window is non-transparent and has black bg color to get better visual effects\n    /// - Otherwise set it to `false`\n    pub enable_gradient: bool,\n}\n\nimpl Default for FireworkConfig {\n    fn default() -> Self {\n        Self {\n            gravity_scale: 1.,\n            ar_scale: 0.28,\n            additional_force: Box::new(move |_| Vec2::ZERO),\n            gradient_scale: |_| 1.,\n            enable_gradient: false,\n        }\n    }\n}\n\nimpl FireworkConfig {\n    /// Set `gradient_scale`\n    #[inline]\n    #[must_use]\n    pub fn with_gradient_scale(mut self, f: fn(f32) -> f32) -> Self {\n        self.gradient_scale = f;\n        self\n    }\n\n    /// Set `gravity_scale`\n    #[inline]\n    #[must_use]\n    pub fn with_gravity_scale(mut self, s: f32) -> Self {\n        self.gravity_scale = s;\n        self\n    }\n\n    /// Set `ar_scale`\n    #[inline]\n    #[must_use]\n    pub fn with_ar_scale(mut self, s: f32) -> Self {\n        self.ar_scale = s;\n        self\n    }\n\n    /// Set `additional_force`\n    #[inline]\n    #[must_use]\n    pub fn with_additional_force(mut self, af: impl Fn(&Particle) -> Vec2 + 'static) -> Self {\n        self.additional_force = Box::new(af);\n        self\n    }\n\n    /// Set `enable_gradient`\n    pub fn set_enable_gradient(&mut self, enable_gradient: bool) {\n        self.enable_gradient = enable_gradient;\n    }\n}\n\n/// `FireworkManager` manages all `Firework`s\npub struct FireworkManager {\n    pub fireworks: Vec<Firework>,\n    /// If this is `true`, the whole fireworks show will restart when all the `Firework`s are `Gone`\n    pub enable_loop: bool,\n    /// Controls how fireworks are installed in `FireworkManager`\n    pub install_form: FireworkInstallForm,\n}\n\nimpl Default for FireworkManager {\n    fn default() -> Self {\n        Self {\n            fireworks: Vec::new(),\n            enable_loop: false,\n            install_form: FireworkInstallForm::StaticInstall,\n        }\n    }\n}\n\nimpl FireworkManager {\n    /// Create a new `FireworkManager` with `enable_loop` set to `false`\n    pub fn new(fireworks: Vec<Firework>) -> Self {\n        Self {\n            fireworks,\n            enable_loop: false,\n            install_form: FireworkInstallForm::StaticInstall,\n        }\n    }\n\n    /// Add a `Firework` to a existing `FireworkManager`\n    pub fn add_firework(&mut self, firework: Firework) {\n        self.fireworks.push(firework);\n    }\n\n    /// Add `Firework`s to a existing `FireworkManager`\n    pub fn add_fireworks(&mut self, mut fireworks: Vec<Firework>) {\n        self.fireworks.append(&mut fireworks);\n    }\n\n    /// Add a `Firework` to `FireworkManager`\n    #[inline]\n    #[must_use]\n    pub fn with_firework(mut self, firework: Firework) -> Self {\n        self.fireworks.push(firework);\n        self\n    }\n\n    // Add a vector of `Firework`s to `FireworkManager`\n    #[inline]\n    #[must_use]\n    pub fn with_fireworks(mut self, mut fireworks: Vec<Firework>) -> Self {\n        self.fireworks.append(&mut fireworks);\n        self\n    }\n\n    /// Set `enable_loop` to `true`\n    #[inline]\n    #[must_use]\n    pub fn enable_loop(mut self) -> Self {\n        self.enable_loop = true;\n        self\n    }\n\n    /// Set `enable_loop` to `false`\n    #[inline]\n    #[must_use]\n    pub fn disable_loop(mut self) -> Self {\n        self.enable_loop = false;\n        self\n    }\n\n    /// Reset the whole fireworks show\n    pub fn reset(&mut self) {\n        for ele in self.fireworks.iter_mut() {\n            ele.reset();\n        }\n    }\n\n    pub fn set_enable_loop(&mut self, enable_loop: bool) {\n        self.enable_loop = enable_loop;\n    }\n\n    /// The main update function\n    pub fn update(&mut self, now: SystemTime, delta_time: Duration) {\n        for ele in self.fireworks.iter_mut() {\n            ele.update(now, delta_time);\n        }\n        if self.install_form == FireworkInstallForm::DynamicInstall {\n            self.fireworks.retain(|f| f.state != FireworkState::Gone);\n        }\n        if self.install_form == FireworkInstallForm::StaticInstall\n            && self.enable_loop\n            && self.fireworks.iter().all(|f| f.is_gone())\n        {\n            self.reset();\n        }\n    }\n\n    /// Set `install_form` to `DynamicInstall`\n    pub fn enable_dyn_install(mut self) -> Self {\n        self.install_form = FireworkInstallForm::DynamicInstall;\n        self\n    }\n}\n\n/// `StaticInstall` keeps all the fireworks in `FireworkManager` and won't delete them\n///\n/// `DynamicInstall` automatically remove fireworks that are `Gone`, which let you add fireworks continuously\n///\n/// # Notes\n///\n///  - `FireworkManager` that has `DynamicInstall` can't loop, it will ignore the set `enable_loop` value\n#[derive(Debug, PartialEq)]\npub enum FireworkInstallForm {\n    StaticInstall,\n    DynamicInstall,\n}\n\nfn init_trail(init_pos: Vec2, n: usize) -> VecDeque<Vec2> {\n    VecDeque::from(vec![init_pos; n])\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "pub mod config;\npub mod demo;\npub mod fireworks;\npub mod particle;\npub mod term;\npub mod utils;\n"
  },
  {
    "path": "src/particle.rs",
    "content": "//! `particle` module provides functions to define, create and update particles\n\nuse std::{collections::VecDeque, time::Duration};\n\nuse glam::Vec2;\n\nuse crate::fireworks::FireworkConfig;\n\n/// The struct represents the states in a `Particle`'s lifetime\n///\n/// Every `Particle` goes from `Alive` -> `Declining` -> `Dying` -> `Dead`\n#[derive(Debug, Clone, Copy, PartialEq)]\npub enum LifeState {\n    Alive,\n    Declining,\n    Dying,\n    Dead,\n}\n\n/// The struct representing a single particle\n#[derive(Debug, Clone)]\npub struct Particle {\n    pub pos: Vec2,\n    pub vel: Vec2,\n    /// Records a `trail_length` of previous positions of the `Particle`\n    pub trail: VecDeque<Vec2>,\n    pub life_state: LifeState,\n    /// `Duration` since initialization of this `Particle`\n    pub time_elapsed: Duration,\n    pub config: ParticleConfig,\n}\n\nimpl Default for Particle {\n    fn default() -> Self {\n        Self {\n            pos: Vec2::ZERO,\n            vel: Vec2::ZERO,\n            trail: VecDeque::new(),\n            life_state: LifeState::Alive,\n            time_elapsed: Duration::ZERO,\n            config: ParticleConfig::default(),\n        }\n    }\n}\n\nimpl Particle {\n    /// Create a new `Particle`\n    pub fn new(\n        pos: Vec2,\n        vel: Vec2,\n        trail_length: usize,\n        life_time: Duration,\n        color: (u8, u8, u8),\n    ) -> Self {\n        let trail = VecDeque::from(vec![pos; trail_length]);\n        let life_state = LifeState::Alive;\n        Self {\n            pos,\n            vel,\n            trail,\n            life_state,\n            time_elapsed: Duration::ZERO,\n            config: ParticleConfig::new(pos, vel, trail_length, life_time, color),\n        }\n    }\n\n    /// Return true if `Particle`'s `LifeState` is `Dead`\n    pub fn is_dead(&self) -> bool {\n        self.life_state == LifeState::Dead\n    }\n\n    /// Reset `Particle` to its initial state\n    pub fn reset(&mut self) {\n        self.pos = self.config.init_pos;\n        self.vel = self.config.init_vel;\n        (0..self.config.trail_length).for_each(|i| self.trail[i] = self.pos);\n        self.life_state = LifeState::Alive;\n        self.time_elapsed = Duration::ZERO;\n    }\n\n    /// Update the `Particle` based on delta time\n    ///\n    /// # Arguments\n    ///\n    /// * - `duration` - `Duration` since last update\n    pub fn update(&mut self, duration: Duration, config: &FireworkConfig) {\n        const TIME_STEP: f32 = 0.001;\n        self.time_elapsed += duration;\n        self.life_state = cal_life_state(self.config.life_time, self.time_elapsed);\n        let mut t = 0.;\n        while t < duration.as_secs_f32() {\n            self.vel += TIME_STEP\n                * (Vec2::Y * 10. * config.gravity_scale\n                    - self.vel.normalize() * self.vel.length().powi(2) * config.ar_scale\n                    + (config.additional_force)(self));\n            self.pos += TIME_STEP * self.vel;\n            t += TIME_STEP;\n        }\n        self.trail.pop_front();\n        self.trail.push_back(self.pos);\n    }\n}\n\n/// Struct that defines the configuration of `Particle`\n#[derive(Debug, Copy, Clone, PartialEq)]\npub struct ParticleConfig {\n    pub init_pos: Vec2,\n    pub init_vel: Vec2,\n    pub trail_length: usize,\n    /// `Duration` from `Particle`'s initialization to its `Dead`\n    pub life_time: Duration,\n    /// Color in RGB (from 0 to 255)\n    pub color: (u8, u8, u8),\n}\n\nimpl Default for ParticleConfig {\n    fn default() -> Self {\n        Self {\n            init_pos: Vec2::ZERO,\n            init_vel: Vec2::ZERO,\n            trail_length: 2,\n            life_time: Duration::from_secs(3),\n            color: (255, 255, 255),\n        }\n    }\n}\n\nimpl ParticleConfig {\n    /// Create a new `ParticleConfig`\n    pub fn new(\n        init_pos: Vec2,\n        init_vel: Vec2,\n        trail_length: usize,\n        life_time: Duration,\n        color: (u8, u8, u8),\n    ) -> Self {\n        Self {\n            init_pos,\n            init_vel,\n            trail_length,\n            life_time,\n            color,\n        }\n    }\n}\n\nfn cal_life_state(life_time: Duration, current_elapsed: Duration) -> LifeState {\n    let p = current_elapsed.as_millis() as f32 / life_time.as_millis() as f32;\n    if p < 0.4 {\n        LifeState::Alive\n    } else if p < 0.65 {\n        LifeState::Declining\n    } else if p < 1. {\n        LifeState::Dying\n    } else {\n        LifeState::Dead\n    }\n}\n"
  },
  {
    "path": "src/term.rs",
    "content": "//! `term` module provides functions of rendering in terminal\n\nuse std::io::{Stdout, Write};\n\nuse crossterm::{cursor::MoveTo, queue, style, terminal};\nuse glam::Vec2;\nuse rand::{seq::IteratorRandom, thread_rng};\n\nuse crate::{\n    config::Config,\n    fireworks::{FireworkManager, FireworkState},\n    particle::LifeState,\n    utils::distance_squared,\n};\n\n/// Wrap a character with color\n#[derive(Debug, Clone, Copy)]\npub struct Char {\n    pub text: char,\n    pub color: style::Color,\n}\n\n#[allow(unused)]\nimpl Char {\n    /// Create a new `Char`\n    fn new(text: char, color: style::Color) -> Self {\n        Self { text, color }\n    }\n}\n\n/// Struct that represents a terminal\npub struct Terminal {\n    pub size: (u16, u16),\n    pub screen: Vec<Vec<Char>>,\n}\n\nimpl Default for Terminal {\n    fn default() -> Self {\n        let size = terminal::size().expect(\"Fail to get terminal size.\");\n        let screen = vec![\n            vec![\n                Char {\n                    text: ' ',\n                    color: style::Color::White\n                };\n                size.0 as usize\n            ];\n            size.1 as usize\n        ];\n        Self { size, screen }\n    }\n}\n\nimpl Terminal {\n    pub fn new(cfg: &Config) -> Self {\n        let mut size = terminal::size().expect(\"Fail to get terminal size.\");\n        if cfg.enable_cjk {\n            size.0 = (size.0 - 1) / 2;\n        }\n        let screen = vec![\n            vec![\n                Char {\n                    text: ' ',\n                    color: style::Color::White\n                };\n                size.0 as usize\n            ];\n            size.1 as usize\n        ];\n        Self { size, screen }\n    }\n\n    /// Reload terminal to adapt new window size\n    pub fn reinit(&mut self, cfg: &Config) {\n        let mut size = terminal::size().expect(\"Fail to get terminal size.\");\n        if cfg.enable_cjk {\n            size.0 = (size.0 - 1) / 2;\n        }\n        self.size = size;\n        self.screen = vec![\n            vec![\n                Char {\n                    text: ' ',\n                    color: style::Color::White\n                };\n                size.0 as usize\n            ];\n            size.1 as usize\n        ];\n    }\n\n    /// Clear the terminal screen by setting all the characters in terminal to space\n    pub fn clear_screen(&mut self) {\n        let size = terminal::size().expect(\"Fail to get terminal size.\");\n        self.screen = vec![\n            vec![\n                Char {\n                    text: ' ',\n                    color: style::Color::White\n                };\n                size.0 as usize\n            ];\n            size.1 as usize\n        ];\n    }\n\n    /// Print the data out to terminal\n    pub fn print(&self, w: &mut Stdout, cfg: &Config) {\n        self.screen.iter().enumerate().for_each(|(y, line)| {\n            line.iter().enumerate().for_each(|(x, c)| {\n                queue!(\n                    w,\n                    MoveTo(\n                        if cfg.enable_cjk {\n                            (x * 2) as u16\n                        } else {\n                            x as u16\n                        },\n                        y as u16\n                    ),\n                    style::SetForegroundColor(c.color),\n                    style::Print(c.text)\n                )\n                .expect(\"Std io error.\")\n            });\n        });\n        w.flush().expect(\"Std io error.\");\n    }\n\n    /// Write the rendering data of all `Fireworks` and `Particles` to `Terminal`\n    pub fn render(&mut self, fm: &FireworkManager, cfg: &Config) {\n        self.clear_screen();\n        for firework in fm.fireworks.iter().rev() {\n            if firework.state == FireworkState::Alive {\n                for particle in firework.current_particles.iter().rev() {\n                    let grad = if firework.config.enable_gradient {\n                        Some((firework.config.gradient_scale)(\n                            particle.time_elapsed.as_secs_f32()\n                                / particle.config.life_time.as_secs_f32(),\n                        ))\n                    } else {\n                        None\n                    };\n                    particle\n                        .trail\n                        .iter()\n                        .map(|p| {\n                            if cfg.enable_cjk {\n                                *p\n                            } else {\n                                Vec2::new(p.x * 2., p.y)\n                            }\n                        })\n                        .rev()\n                        .collect::<Vec<_>>()\n                        .windows(2)\n                        .enumerate()\n                        .for_each(|(idx, v)| {\n                            let density = (particle.config.trail_length - idx - 1) as f32\n                                / particle.config.trail_length as f32;\n                            construct_line(v[0], v[1]).iter().for_each(|p| {\n                                if self.inside(*p)\n                                    && self.screen[p.1 as usize][p.0 as usize].text == ' '\n                                {\n                                    if let Some(c) = match particle.life_state {\n                                        LifeState::Alive => {\n                                            Some(get_char_alive(density, cfg.enable_cjk))\n                                        }\n                                        LifeState::Declining => {\n                                            Some(get_char_declining(density, cfg.enable_cjk))\n                                        }\n                                        LifeState::Dying => {\n                                            Some(get_char_dying(density, cfg.enable_cjk))\n                                        }\n                                        LifeState::Dead => None,\n                                    } {\n                                        self.screen[p.1 as usize][p.0 as usize] = Char {\n                                            text: c,\n                                            color: {\n                                                let color_u8 = if let Some(g) = grad {\n                                                    shift_gradient(particle.config.color, g)\n                                                } else {\n                                                    particle.config.color\n                                                };\n                                                style::Color::Rgb {\n                                                    r: color_u8.0,\n                                                    g: color_u8.1,\n                                                    b: color_u8.2,\n                                                }\n                                            },\n                                        }\n                                    }\n                                }\n                            });\n                        });\n                }\n            }\n        }\n    }\n\n    fn inside(&self, (x, y): (isize, isize)) -> bool {\n        x < self.size.0 as isize && y < self.size.1 as isize && x >= 0 && y >= 0\n    }\n}\n\nfn construct_line(a: Vec2, b: Vec2) -> Vec<(isize, isize)> {\n    const STEP: f32 = 0.2;\n    let (x0, y0) = (a.x, a.y);\n    let (x1, y1) = (b.x, b.y);\n    let mut path = Vec::new();\n    let mut x = x0;\n    let mut y = y0;\n    let slope = (y1 - y0) / (x1 - x0);\n    let dx = if x0 == x1 {\n        0.\n    } else if x1 > x0 {\n        1.\n    } else {\n        -1.\n    };\n    let dy = if y0 == y1 {\n        0.\n    } else if y1 > y0 {\n        1.\n    } else {\n        -1.\n    };\n    let mut ds = distance_squared(a, b) + f32::EPSILON;\n    path.push((x0.round() as isize, y0.round() as isize));\n    if (x1 - x0).abs() >= (y1 - y0).abs() {\n        while distance_squared(Vec2::new(x, y), b) <= ds {\n            if *path.last().unwrap() != (x.round() as isize, y.round() as isize) {\n                path.push((x.round() as isize, y.round() as isize));\n                ds = distance_squared(Vec2::new(x, y), b);\n            }\n            x += dx * STEP;\n            y += dy * (STEP * slope).abs();\n        }\n    } else {\n        while distance_squared(Vec2::new(x, y), b) <= ds {\n            if *path.last().unwrap() != (x.round() as isize, y.round() as isize) {\n                path.push((x.round() as isize, y.round() as isize));\n                ds = distance_squared(Vec2::new(x, y), b);\n            }\n            y += dy * STEP;\n            x += dx * (STEP / slope).abs();\n        }\n    }\n    path\n}\n\nfn shift_gradient(color: (u8, u8, u8), scale: f32) -> (u8, u8, u8) {\n    (\n        (color.0 as f32 * scale) as u8,\n        (color.1 as f32 * scale) as u8,\n        (color.2 as f32 * scale) as u8,\n    )\n}\n\nfn get_char_alive(density: f32, cjk: bool) -> char {\n    let palette = if density < 0.3 {\n        if cjk {\n            \"。，”“』 『￥\"\n        } else {\n            \"`'. \"\n        }\n    } else if density < 0.5 {\n        if cjk {\n            \"一二三二三五十十已于上下义天\"\n            // \"いうよへくひとフーク \"\n        } else {\n            \"/\\\\|()1{}[]?\"\n        }\n    } else if density < 0.7 {\n        if cjk {\n            \"时中自字木月日目火田左右点以\"\n            // \"探しているのが誰かなのかどこかなのかそれともただ単に就職先なのか自分でもよくわからない\"\n        } else {\n            \"oahkbdpqwmZO0QLCJUYXzcvunxrjft*\"\n        }\n    } else if cjk {\n        \"龖龠龜\"\n        // \"東京福岡横浜縄\"\n    } else {\n        \"$@B%8&WM#\"\n    };\n    palette\n        .chars()\n        .choose(&mut thread_rng())\n        .expect(\"Fail to choose character.\")\n}\n\nfn get_char_declining(density: f32, cjk: bool) -> char {\n    let palette = if density < 0.2 {\n        if cjk {\n            \"？。， 『』 ||\"\n        } else {\n            \"` '. \"\n        }\n    } else if density < 0.6 {\n        if cjk {\n            \"（）【】*￥|十一二三六\"\n            // \"（）【】*￥|ソファー\"\n        } else {\n            \"-_ +~<> i!lI;:,\\\"^\"\n        }\n    } else if density < 0.85 {\n        if cjk {\n            \"人中亿入上下火土\"\n            // \"人ならざるものに出会うかもしれない\"\n        } else {\n            \"/\\\\| ()1{}[ ]?\"\n        }\n    } else if cjk {\n        \"繁荣昌盛国泰民安龍龖龠龜耋\"\n        // \"時間言葉目覚\"\n    } else {\n        \"xrjft*\"\n    };\n    palette\n        .chars()\n        .choose(&mut thread_rng())\n        .expect(\"Fail to choose character.\")\n}\n\nfn get_char_dying(density: f32, cjk: bool) -> char {\n    let palette = if density < 0.6 {\n        if cjk {\n            \"。 『 』 、： |。，— ……\"\n        } else {\n            \".  ,`.    ^,' . \"\n        }\n    } else if cjk {\n        \"|￥人 上十入乙小 下\"\n        // \"イントマトナイフ\"\n    } else {\n        \" /\\\\| ( )  1{} [  ]?i !l I;: ,\\\"^ \"\n    };\n    palette\n        .chars()\n        .choose(&mut thread_rng())\n        .expect(\"Fail to choose character.\")\n}\n"
  },
  {
    "path": "src/utils.rs",
    "content": "//! `utils` module provides some useful helper functions of random generation and gradient scale\n\nuse std::f32::consts::PI;\n\nuse glam::Vec2;\nuse rand::Rng;\nuse rand_distr::Distribution;\n\n/// Round a `Vec2` from `(f32, f32)` to `(isize, isize)`\npub fn round(input: Vec2) -> (isize, isize) {\n    (input.x.round() as isize, input.y.round() as isize)\n}\n\n/// Generate random `Vec2` within a circle range\npub fn gen_points_circle(radius: isize, n: usize) -> Vec<Vec2> {\n    let mut res = Vec::new();\n    while res.len() < n {\n        let x = rand::thread_rng().gen_range(-radius..=radius);\n        let y = rand::thread_rng().gen_range(-radius..=radius);\n        if x.pow(2) + y.pow(2) <= radius.pow(2) {\n            res.push(Vec2::new(x as f32, y as f32));\n        }\n    }\n    res\n}\n\n/// Generate random `Vec2` within a circle range with normal distribution\n///\n/// Points closer to the center will be denser\npub fn gen_points_circle_normal(radius: f32, n: usize) -> Vec<Vec2> {\n    let mut rng = rand::thread_rng();\n    let normal =\n        rand_distr::Normal::new(0., radius / 9.).expect(\"Unable to generate normal distribution.\");\n    let mut res = Vec::new();\n    while res.len() < n {\n        let x = normal.sample(&mut rng);\n        if x < -radius || x > radius {\n            continue;\n        }\n        let y = normal.sample(&mut rng);\n        if x < -radius || y > radius {\n            continue;\n        }\n        if x.powi(2) + y.powi(2) <= radius.powi(2) {\n            res.push(Vec2::new(x, y));\n        }\n    }\n    res\n}\n\n/// Generate random `Vec2` within a circle range with normal distribution\n///\n/// Points closer to the center will be denser\n/// You can specify standard deviation yourself\npub fn gen_points_circle_normal_dev(radius: f32, n: usize, std_dev: f32) -> Vec<Vec2> {\n    let mut rng = rand::thread_rng();\n    let normal =\n        rand_distr::Normal::new(0., std_dev).expect(\"Unable to generate normal distribution.\");\n    let mut res = Vec::new();\n    while res.len() < n {\n        let x = normal.sample(&mut rng);\n        if x < -radius || x > radius {\n            continue;\n        }\n        let y = normal.sample(&mut rng);\n        if x < -radius || y > radius {\n            continue;\n        }\n        if x.powi(2) + y.powi(2) <= radius.powi(2) {\n            res.push(Vec2::new(x, y));\n        }\n    }\n    res\n}\n\n/// Generate random `Vec2` within a fan-shape range\npub fn gen_points_fan(radius: f32, n: usize, st_angle: f32, ed_angle: f32) -> Vec<Vec2> {\n    let mut res = Vec::new();\n    while res.len() < n {\n        let x = rand::thread_rng().gen_range(-radius..=radius);\n        let y = rand::thread_rng().gen_range(-radius..=radius);\n        let t = y.atan2(x);\n        if t <= ed_angle && t >= st_angle && x.powi(2) + y.powi(2) <= radius.powi(2) {\n            res.push(Vec2::new(x, -y));\n        }\n    }\n    res\n}\n\n/// Generate random `Vec2` on an arc\npub fn gen_points_arc(radius: f32, n: usize, st_angle: f32, ed_angle: f32) -> Vec<Vec2> {\n    let mut res = Vec::new();\n    while res.len() < n {\n        let a = rand::thread_rng().gen_range(st_angle..=ed_angle);\n        res.push(Vec2::new(radius * a.cos(), -radius * a.sin()));\n    }\n    res\n}\n\n/// Generate random `Vec2` on a circle\npub fn gen_points_on_circle(radius: f32, n: usize) -> Vec<Vec2> {\n    let mut res = Vec::new();\n    while res.len() < n {\n        let a = rand::thread_rng().gen_range(0.0..PI);\n        res.push(Vec2::new(radius * a.cos(), -radius * a.sin()));\n    }\n    res\n}\n\n/// Return squared distance between to points\npub fn distance_squared(a: Vec2, b: Vec2) -> f32 {\n    (b.x - a.x).powi(2) + (b.y - a.y).powi(2)\n}\n\n/// A sample function defining the gradient of the `Particle`\n///\n/// The visual effect is similar to an explosion\npub fn explosion_gradient_1(x: f32) -> f32 {\n    if x < 0.087 {\n        150. * x.powi(2)\n    } else {\n        -0.8 * x + 1.2\n    }\n}\n\n/// A sample function defining the gradient of the `Particle`\n///\n/// The visual effect is similar to an explosion\npub fn explosion_gradient_2(x: f32) -> f32 {\n    if x < 0.067 {\n        5. * x + 0.1\n    } else if x < 0.2 {\n        2. * x + 0.3\n    } else if x < 0.5 {\n        x + 0.5\n    } else if x < 0.684 {\n        0.5 * x + 0.75\n    } else {\n        -7. * (x - 0.65).powi(2) + 1.1\n    }\n}\n\n/// A sample function defining the gradient of the `Particle`\n///\n/// The visual effect is similar to an explosion, darkar than `explosion_gradient_1`\npub fn explosion_gradient_3(x: f32) -> f32 {\n    if x < 0.087 {\n        150. * x.powi(2) * 0.6\n    } else {\n        (-0.8 * x + 1.2) * 0.6\n    }\n}\n\n/// A sample function defining the gradient of the `Particle`\n///\n/// Linear gradient\npub fn linear_gradient_1(x: f32) -> f32 {\n    -0.7 * x + 1.\n}\n"
  }
]