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 <https://github.com/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
================================================
<h1 align="center">
<br>
<img src="https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/demo_0.gif" alt="gif" width="800">
<br>
<br>
Firework-rs
<br>
</h1>
<p align="center">
<a href="https://crates.io/crates/firework-rs"><img alt="crates.io" src="https://img.shields.io/crates/v/firework-rs.svg"></a>
<a><img alt="License" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
</p>
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 <DEMO-NUMBER>
Options:
-d, --demo <DEMO-NUMBER>
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 <FRAME-RATE>
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>
```
**Example-name** contains:
fountain
<h4 align="center">
<img src="https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/fountain.gif" alt="gif" width="600">
</h4>
vortex
<h4 align="center">
<img src="https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/vortex.gif" alt="gif" width="600">
</h4>
heart
<h4 align="center">
<img src="https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/heart.gif" alt="gif" width="600">
</h4>
## 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<u8>,
/// 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<u8>,
/// 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<Firework> {
// 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<Firework> {
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<Firework> {
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<Firework> {
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<ParticleConfig>,
pub current_particles: Vec<Particle>,
}
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<dyn Fn(&Particle) -> 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<Firework>,
/// 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<Firework>) -> 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<Firework>) {
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<Firework>) -> 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<Vec2> {
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<Vec2>,
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<Vec<Char>>,
}
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::<Vec<_>>()
.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<Vec2> {
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<Vec2> {
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<Vec2> {
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<Vec2> {
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<Vec2> {
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<Vec2> {
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.
}
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
SYMBOL INDEX (91 symbols across 12 files)
FILE: examples/fountain.rs
function main (line 23) | fn main() -> Result<()> {
function gen_fountain_firework (line 74) | fn gen_fountain_firework(center: Vec2) -> Firework {
function gradient (line 113) | fn gradient(x: f32) -> f32 {
FILE: examples/heart.rs
function main (line 23) | fn main() -> Result<()> {
function gen_heart_firework (line 74) | fn gen_heart_firework(center: Vec2) -> Firework {
function gradient (line 120) | fn gradient(x: f32) -> f32 {
FILE: examples/vortex.rs
function main (line 22) | fn main() -> Result<()> {
function gen_vortex_firework (line 73) | fn gen_vortex_firework(center: Vec2) -> Firework {
function gradient (line 114) | fn gradient(x: f32) -> f32 {
FILE: src/bin/firework/args.rs
type Cli (line 6) | pub struct Cli {
FILE: src/bin/firework/gen.rs
function dyn_gen (line 7) | pub fn dyn_gen(
FILE: src/bin/firework/main.rs
function main (line 31) | fn main() -> Result<()> {
FILE: src/config.rs
type Config (line 3) | pub struct Config {
FILE: src/demo.rs
function demo_firework_0 (line 22) | pub fn demo_firework_0(
function demo_firework_1 (line 60) | pub fn demo_firework_1(center: Vec2, spawn_after: Duration, enable_gradi...
function demo_firework_2 (line 89) | pub fn demo_firework_2(center: Vec2, spawn_after: Duration, enable_gradi...
function demo_firework_3 (line 116) | pub fn demo_firework_3(center: Vec2, spawn_after: Duration, enable_gradi...
function demo_firework_4 (line 148) | pub fn demo_firework_4(center: Vec2, spawn_after: Duration, enable_gradi...
function demo_firework_5 (line 174) | pub fn demo_firework_5(center: Vec2, spawn_after: Duration, enable_gradi...
function demo_firework_6 (line 200) | pub fn demo_firework_6(center: Vec2, spawn_after: Duration, enable_gradi...
function demo_firework_comb_1 (line 227) | pub fn demo_firework_comb_1(
function demo_firework_comb_0 (line 290) | pub fn demo_firework_comb_0(
function demo_firework_comb_2 (line 320) | pub fn demo_firework_comb_2(
function demo_firework_comb_3 (line 445) | pub fn demo_firework_comb_3(
FILE: src/fireworks.rs
type Firework (line 14) | pub struct Firework {
method update (line 51) | pub fn update(&mut self, now: SystemTime, delta_time: Duration) {
method is_gone (line 133) | pub fn is_gone(&self) -> bool {
method reset (line 138) | pub fn reset(&mut self) {
method default (line 29) | fn default() -> Self {
type FireworkState (line 163) | pub enum FireworkState {
type ExplosionForm (line 172) | pub enum ExplosionForm {
type FireworkConfig (line 188) | pub struct FireworkConfig {
method with_gradient_scale (line 226) | pub fn with_gradient_scale(mut self, f: fn(f32) -> f32) -> Self {
method with_gravity_scale (line 234) | pub fn with_gravity_scale(mut self, s: f32) -> Self {
method with_ar_scale (line 242) | pub fn with_ar_scale(mut self, s: f32) -> Self {
method with_additional_force (line 250) | pub fn with_additional_force(mut self, af: impl Fn(&Particle) -> Vec2 ...
method set_enable_gradient (line 256) | pub fn set_enable_gradient(&mut self, enable_gradient: bool) {
method default (line 211) | fn default() -> Self {
type FireworkManager (line 262) | pub struct FireworkManager {
method new (line 282) | pub fn new(fireworks: Vec<Firework>) -> Self {
method add_firework (line 291) | pub fn add_firework(&mut self, firework: Firework) {
method add_fireworks (line 296) | pub fn add_fireworks(&mut self, mut fireworks: Vec<Firework>) {
method with_firework (line 303) | pub fn with_firework(mut self, firework: Firework) -> Self {
method with_fireworks (line 311) | pub fn with_fireworks(mut self, mut fireworks: Vec<Firework>) -> Self {
method enable_loop (line 319) | pub fn enable_loop(mut self) -> Self {
method disable_loop (line 327) | pub fn disable_loop(mut self) -> Self {
method reset (line 333) | pub fn reset(&mut self) {
method set_enable_loop (line 339) | pub fn set_enable_loop(&mut self, enable_loop: bool) {
method update (line 344) | pub fn update(&mut self, now: SystemTime, delta_time: Duration) {
method enable_dyn_install (line 360) | pub fn enable_dyn_install(mut self) -> Self {
method default (line 271) | fn default() -> Self {
type FireworkInstallForm (line 374) | pub enum FireworkInstallForm {
function init_trail (line 379) | fn init_trail(init_pos: Vec2, n: usize) -> VecDeque<Vec2> {
FILE: src/particle.rs
type LifeState (line 13) | pub enum LifeState {
type Particle (line 22) | pub struct Particle {
method new (line 48) | pub fn new(
method is_dead (line 68) | pub fn is_dead(&self) -> bool {
method reset (line 73) | pub fn reset(&mut self) {
method update (line 86) | pub fn update(&mut self, duration: Duration, config: &FireworkConfig) {
method default (line 34) | fn default() -> Self {
type ParticleConfig (line 106) | pub struct ParticleConfig {
method new (line 130) | pub fn new(
method default (line 117) | fn default() -> Self {
function cal_life_state (line 147) | fn cal_life_state(life_time: Duration, current_elapsed: Duration) -> Lif...
FILE: src/term.rs
type Char (line 18) | pub struct Char {
method new (line 26) | fn new(text: char, color: style::Color) -> Self {
type Terminal (line 32) | pub struct Terminal {
method new (line 55) | pub fn new(cfg: &Config) -> Self {
method reinit (line 74) | pub fn reinit(&mut self, cfg: &Config) {
method clear_screen (line 93) | pub fn clear_screen(&mut self) {
method print (line 108) | pub fn print(&self, w: &mut Stdout, cfg: &Config) {
method render (line 131) | pub fn render(&mut self, fm: &FireworkManager, cfg: &Config) {
method inside (line 201) | fn inside(&self, (x, y): (isize, isize)) -> bool {
method default (line 38) | fn default() -> Self {
function construct_line (line 206) | fn construct_line(a: Vec2, b: Vec2) -> Vec<(isize, isize)> {
function shift_gradient (line 252) | fn shift_gradient(color: (u8, u8, u8), scale: f32) -> (u8, u8, u8) {
function get_char_alive (line 260) | fn get_char_alive(density: f32, cjk: bool) -> char {
function get_char_declining (line 293) | fn get_char_declining(density: f32, cjk: bool) -> char {
function get_char_dying (line 326) | fn get_char_dying(density: f32, cjk: bool) -> char {
FILE: src/utils.rs
function round (line 10) | pub fn round(input: Vec2) -> (isize, isize) {
function gen_points_circle (line 15) | pub fn gen_points_circle(radius: isize, n: usize) -> Vec<Vec2> {
function gen_points_circle_normal (line 30) | pub fn gen_points_circle_normal(radius: f32, n: usize) -> Vec<Vec2> {
function gen_points_circle_normal_dev (line 55) | pub fn gen_points_circle_normal_dev(radius: f32, n: usize, std_dev: f32)...
function gen_points_fan (line 77) | pub fn gen_points_fan(radius: f32, n: usize, st_angle: f32, ed_angle: f3...
function gen_points_arc (line 91) | pub fn gen_points_arc(radius: f32, n: usize, st_angle: f32, ed_angle: f3...
function gen_points_on_circle (line 101) | pub fn gen_points_on_circle(radius: f32, n: usize) -> Vec<Vec2> {
function distance_squared (line 111) | pub fn distance_squared(a: Vec2, b: Vec2) -> f32 {
function explosion_gradient_1 (line 118) | pub fn explosion_gradient_1(x: f32) -> f32 {
function explosion_gradient_2 (line 129) | pub fn explosion_gradient_2(x: f32) -> f32 {
function explosion_gradient_3 (line 146) | pub fn explosion_gradient_3(x: f32) -> f32 {
function linear_gradient_1 (line 157) | pub fn linear_gradient_1(x: f32) -> f32 {
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (86K chars).
[
{
"path": ".github/workflows/build.yml",
"chars": 1838,
"preview": "name: Build\non:\n pull_request:\n push:\n branches:\n - master\n - v0.3.0\n schedule:\n - cron: '00 01 * * *"
},
{
"path": ".github/workflows/release.yml",
"chars": 4447,
"preview": "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"
},
{
"path": ".gitignore",
"chars": 16,
"preview": "/target\n.vscode\n"
},
{
"path": "CHANGELOG.md",
"chars": 825,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [0.3.1](https://github.com/Wayoung"
},
{
"path": "Cargo.toml",
"chars": 784,
"preview": "[package]\nname = \"firework-rs\"\nversion = \"0.3.1\"\nauthors = [\"Wayoung7 <https://github.com/Wayoung7>\"]\nedition = \"2021\"\nd"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2024 Wayoung7\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 6182,
"preview": "<h1 align=\"center\">\n<br>\n<img src=\"https://raw.githubusercontent.com/Wayoung7/firework-rs/master/gif/demo_0.gif\" alt=\"gi"
},
{
"path": "examples/fountain.rs",
"chars": 3190,
"preview": "use std::{\n f32::consts::PI,\n io::{stdout, Result},\n thread::sleep,\n time::{Duration, SystemTime},\n};\n\nuse c"
},
{
"path": "examples/heart.rs",
"chars": 3489,
"preview": "use std::{\n f32::consts::PI,\n io::{stdout, Result},\n thread::sleep,\n time::{Duration, SystemTime},\n};\n\nuse c"
},
{
"path": "examples/vortex.rs",
"chars": 3336,
"preview": "use std::{\n io::{stdout, Result},\n thread::sleep,\n time::{Duration, SystemTime},\n};\n\nuse crossterm::{\n curso"
},
{
"path": "src/bin/firework/args.rs",
"chars": 1048,
"preview": "use clap::Parser;\n\n/// Used to receive command line arguments\n#[derive(Parser)]\n#[command(version, about, long_about = N"
},
{
"path": "src/bin/firework/gen.rs",
"chars": 2139,
"preview": "use std::time::Duration;\n\nuse firework_rs::{config::Config, demo::demo_firework_0, fireworks::FireworkManager};\nuse glam"
},
{
"path": "src/bin/firework/main.rs",
"chars": 4256,
"preview": "//! With the `firework` binary, you can run some pre-designed fireworks with command line arguments\n\nmod args;\nmod gen;\n"
},
{
"path": "src/config.rs",
"chars": 100,
"preview": "/// Configuration of the program\n#[derive(Default)]\npub struct Config {\n pub enable_cjk: bool,\n}\n"
},
{
"path": "src/demo.rs",
"chars": 17358,
"preview": "//! This module provides some demos of different types of fireworks\n\nuse std::{\n f32::consts::PI,\n time::{Duration"
},
{
"path": "src/fireworks.rs",
"chars": 12168,
"preview": "//! `firework` module provides functions to define, create and update fireworks\n\nuse std::{\n collections::VecDeque,\n "
},
{
"path": "src/lib.rs",
"chars": 96,
"preview": "pub mod config;\npub mod demo;\npub mod fireworks;\npub mod particle;\npub mod term;\npub mod utils;\n"
},
{
"path": "src/particle.rs",
"chars": 4373,
"preview": "//! `particle` module provides functions to define, create and update particles\n\nuse std::{collections::VecDeque, time::"
},
{
"path": "src/term.rs",
"chars": 10703,
"preview": "//! `term` module provides functions of rendering in terminal\n\nuse std::io::{Stdout, Write};\n\nuse crossterm::{cursor::Mo"
},
{
"path": "src/utils.rs",
"chars": 4715,
"preview": "//! `utils` module provides some useful helper functions of random generation and gradient scale\n\nuse std::f32::consts::"
}
]
About this extraction
This page contains the full source code of the Wayoung7/firework-rs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (80.2 KB), approximately 21.2k tokens, and a symbol index with 91 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.