Animated ASCII cityscape for the terminal, written in Rust. Renders a procedurally animated rooftop view of a city skyline with buildings, traffic, planes, helicopters, birds, clouds, weather, and a day/night cycle. Single-purpose tool.
- Rust (edition 2024)
- crossterm - terminal control (raw mode, input, resize)
- ratatui - rendering (buffer diffing, styled cells)
- clap - CLI argument parsing (derive macros)
- rand - random number generation
CLI (main.rs) -> SceneConfig -> Engine (engine.rs) -> CityscapeScene (cityscape/)
Art pipeline:
assets/*.txt --include_str!--> cityscape/art.rs (defaults)
~/.config/asciicity/ --runtime load--> art.rs (overrides)
|
v
ArtData { frames: Vec<String>, colors: Option<ColorMap> }
Color pipeline:
.colors file (char-based palette) --> ColorMap::Palette
.colormap file (positional grid) --> ColorMap::Grid
|
v
draw_ascii_styled() applies per-character fg colors
Behavior systems:
Wind - smooth gusting, affects entity drift
DayNight - sky color keyframes, ambient light, star visibility
Parallax - camera scroll with per-layer depth offsets
Weather - rain/snow/fog particle spawning
Rendering:
Layer compositing (back-to-front, with parallax offsets)
- Engine - main loop: poll input, tick scene, render via ratatui, sleep. Takes a
SceneConfigand passes it toScene::setup. TheScenetrait is the engine/scene boundary, not a multi-scene hook: there is one scene,CityscapeScene. - Scene trait -
setup(width, height, cfg, rng),tick(dt, rng),render(frame),resize().SceneConfigcarries CLI spawn-rate multipliers, weather override, and day/night time settings. The scene stores its config soresize()can re-apply it. - Art loader (
art.rs) - loads art from embedded defaults or user overrides in~/.config/asciicity/, returnsArtData.mirror_horizontal()flips art for direction-aware entities. - Layer - 2D grid of optional styled cells, composited back-to-front.
composite_offset()iterates the full layer dimensions (not screen-clamped) so wide parallax layers draw their off-screen content when panned into view. - Entity - position/velocity/frames/style +
tag: u32(type discriminator, e.g. cloud/plane/heli/bird/car),meta: f64(per-entity scalar, e.g. cloud brightness bias), andbob_amp/freq/phasefor sinusoidal vertical motion on top ofvydrift. - Color (
color.rs) -ColorMapenum (Palette/Grid), color math utilities (lerp_rgb,fade_rgb), hex/palette/colormap parsers. - Behaviors (
behavior/) - wind, day/night, parallax, weather systems. The cityscape embeds and ticks them.WeathersupportsRain,Snow,Fog, andThunder(rain particles + periodic lightning bolts with sky flash).
CityscapeScene owns its layers, entities, spawners, behavior system instances, and a cloned SceneConfig.
cargo build # build
cargo run # run the cityscape
cargo run -- --fps 15 # adjust frame rate
cargo run -- --car-rate 3 --weather rain # busier cars + rain
cargo run -- --weather thunder # thunderstorm with lightning
cargo run -- --cloud-direction left # clouds drift right-to-left only
cargo run -- --time-speed 2 --start-time 5 # fast day/night starting at sunrise
cargo run -- --warmup 5 # first frame already populated
cargo install --path . # install system-wideFull invocation with every flag explicit at its default:
cargo run -- --fps 15 --cloud-rate 1.0 --plane-rate 1.0 --heli-rate 1.0 --bird-rate 1.0 --car-rate 1.0 --cloud-direction both --far-pan auto --near-pan auto --weather-intensity 1.0 --time-speed 0.2 --start-time 20.0 --warmup 0.0Press any key to exit.
Run these while iterating and before declaring any change done:
cargo check # fast type-check during iteration
cargo clippy -- -D warnings # lint, treat warnings as errors
cargo fmt --check # formatting check (use `cargo fmt` to apply)
cargo test # run tests
cargo build --release # final build sanity-checkPre-commit checklist (do not skip, do not --no-verify):
cargo fmtappliedcargo clippy -- -D warningscleancargo testpasses- No stray
dbg!, debugprintln!, or commented-out code - No new
unwrap()orpanic!/todo!/unimplemented!in merged code paths
When iterating on a compiler/borrow-checker error, paste the full rustc output including the E0xxx code into the conversation rather than paraphrasing. Do not silence clippy warnings with #[allow(...)] unless the suppression is justified in a comment.
- No
unwrap()in production paths. Use?to propagate, or.expect("why this invariant holds")only when an invariant genuinely cannot fail. Same rule forpanic!/todo!/unimplemented!. - No
unsafewithout a// SAFETY:comment spelling out the invariants the caller must uphold. - Prefer borrowing (
&T,&mut T) over owning when the function does not need ownership. Call.clone()explicitly and only when actually necessary. - Prefer iterators and combinators (
map/filter/fold/enumerate) over manual index loops. - Prefer
if let/while letfor single-pattern matches instead of fullmatchblocks. - No wildcard imports (
use foo::*) exceptuse super::*;inside#[cfg(test)]modules. - Naming:
snake_casefor fns/vars/modules,PascalCasefor types/traits,SCREAMING_SNAKE_CASEfor consts. Rustfmt defaults (100-col) are authoritative, do not hand-reformat against them. - Scope fixes narrowly. When fixing a bug or a clippy warning, do not drive-by refactor unrelated code in the same change.
- The cityscape implementation lives in
src/cityscape/asmod.rs+art.rs. CityscapeSceneimplements theScenetrait (insrc/scene.rs). The trait is the engine/scene boundary, not a multi-scene hook.- Entity frames use
Vec<String>, not&'static str(no Box::leak). - Art lives in
assets/*.txt, loaded viainclude_str!insrc/cityscape/art.rs. - Multi-frame animations use
---line separator in.txtfiles. - Optional
.colorsfiles map characters to hex colors (e.g.O #FFD700). - Optional
.colormapfiles provide positional color grids (@palette+@mapsections). - User overrides go in
~/.config/asciicity/(same filenames as underassets/). - Scratch layers are pre-allocated and reused via
.clear(), never allocated per-frame. art::mirror_horizontal()flips art left/right for entities traveling the opposite direction.- ASCII art reference sites listed in
docs/ascii-art-resources.md(gitignored, local only). - Parallax layers must have enough extra width for at least 100px of max shift on the nearest parallax layer. Far layers scale proportionally by depth ratio. Use large PARALLAX_RANGE (~200) for noticeable drift.
- Building colors must use
lerp_rgbfor smooth day/night transitions, never binary if/else. - Entity
tag/metaare the generic way to discriminate and parameterize entities. Use them instead of stuffing state intoframe_intervalor cloning sibling Vecs. - Flying entities (planes, helis, birds) should set
bob_amp/freq/phaseso they don't travel in flat lines. - Vehicle-like entities share the 9-color palette in
cityscape/mod.rs::VEHICLE_PALETTEviapick_vehicle_color. - Direction-aware entities (clouds, birds, planes, helis, cars) must spawn both directions when their config allows it: random
going_right, mirror art viaart::mirror_framesif the source faces the wrong way, and flip the sign ofvx. - Runtime options are read from the
&SceneConfigpassed tosetup(), cloned into the scene struct, and used viascene::scale_intervalwhen computing spawn delays so--*-rate 0disables that entity cleanly.