|
| 1 | +# Asciiquarium (Rust + egui) |
| 2 | + |
| 3 | +A stateless, themeable Asciiquarium widget for `egui`. The widget renders a classic ASCII aquarium using a single `egui::Label`, while your parent application owns and updates the animation state. |
| 4 | + |
| 5 | +- Stateless: The widget only renders from state and assets. |
| 6 | +- Themeable: No hardcoded styles. Colors and wrapping are derived from a theme you pass in. |
| 7 | +- Simple physics: Fish move with velocity and bounce off the aquarium edges. |
| 8 | +- No panics, no unwrap/expect, no unnecessary clones. |
| 9 | + |
| 10 | +Archived originals from the Perl-based Asciiquarium are kept under `archive/original/` for reference. |
| 11 | + |
| 12 | +## Quickstart |
| 13 | + |
| 14 | +1) Add the dependency in your `Cargo.toml`: |
| 15 | + |
| 16 | + [dependencies] |
| 17 | + egui = "0.27" |
| 18 | + asciiquarium_rust = { path = "." } # or from your git repo/registry |
| 19 | + |
| 20 | +2) Prepare assets and state in your app: |
| 21 | + |
| 22 | + use asciiquarium_rust::{ |
| 23 | + get_fish_assets, update_aquarium, AsciiquariumTheme, AsciiquariumWidget, |
| 24 | + AquariumState, FishInstance, |
| 25 | + }; |
| 26 | + |
| 27 | + // Build assets once (e.g., at startup). |
| 28 | + let assets = get_fish_assets(); |
| 29 | + |
| 30 | + // Create your aquarium state (owned by the parent app). |
| 31 | + let mut state = AquariumState { |
| 32 | + size: (80, 24), // character grid width x height |
| 33 | + fishes: vec![ |
| 34 | + FishInstance { |
| 35 | + fish_art_index: 0, |
| 36 | + position: (2.0, 3.0), |
| 37 | + velocity: (0.4, 0.0), |
| 38 | + }, |
| 39 | + FishInstance { |
| 40 | + fish_art_index: 1, |
| 41 | + position: (30.0, 10.0), |
| 42 | + velocity: (-0.3, 0.1), |
| 43 | + }, |
| 44 | + ], |
| 45 | + }; |
| 46 | + |
| 47 | +3) In your app’s update loop, update and render: |
| 48 | + |
| 49 | + // Update the simulation (e.g., once per frame or on your own tick). |
| 50 | + update_aquarium(&mut state, &assets); |
| 51 | + |
| 52 | + // Derive styles from your theme (no hardcoded styles). |
| 53 | + let theme = AsciiquariumTheme { |
| 54 | + text_color: egui::Color32::from_rgb(180, 220, 255), |
| 55 | + background: Some(egui::Color32::from_rgb(8, 12, 16)), |
| 56 | + wrap: false, // keep ASCII grid alignment |
| 57 | + }; |
| 58 | + |
| 59 | + // Render: a single monospace label with your aquarium. |
| 60 | + ui.add(AsciiquariumWidget { |
| 61 | + state: &state, |
| 62 | + assets: &assets, |
| 63 | + theme: &theme, |
| 64 | + }); |
| 65 | + |
| 66 | +## Theming |
| 67 | + |
| 68 | +Follow the “Design for Theming” rule: the component does not hardcode colors or styles. Everything flows through `AsciiquariumTheme`. |
| 69 | + |
| 70 | +- `text_color`: The color used for the ASCII characters. |
| 71 | +- `background`: Optional background fill for the label area. |
| 72 | +- `wrap`: Line wrapping for the label (usually `false` to preserve ASCII alignment). |
| 73 | + |
| 74 | +Example themes: |
| 75 | + |
| 76 | + // Light theme |
| 77 | + let light = AsciiquariumTheme { |
| 78 | + text_color: egui::Color32::from_rgb(40, 40, 40), |
| 79 | + background: Some(egui::Color32::from_rgb(245, 245, 245)), |
| 80 | + wrap: false, |
| 81 | + }; |
| 82 | + |
| 83 | + // High contrast |
| 84 | + let high_contrast = AsciiquariumTheme { |
| 85 | + text_color: egui::Color32::WHITE, |
| 86 | + background: Some(egui::Color32::BLACK), |
| 87 | + wrap: false, |
| 88 | + }; |
| 89 | + |
| 90 | +## API Overview |
| 91 | + |
| 92 | +- `FishArt`: |
| 93 | + - `art: &'static str` |
| 94 | + - `width: usize` |
| 95 | + - `height: usize` |
| 96 | + |
| 97 | +- `FishInstance`: |
| 98 | + - `fish_art_index: usize` |
| 99 | + - `position: (f32, f32)` // top-left in character coordinates |
| 100 | + - `velocity: (f32, f32)` // characters per tick |
| 101 | + |
| 102 | +- `AquariumState`: |
| 103 | + - `size: (usize, usize)` // width x height in characters |
| 104 | + - `fishes: Vec<FishInstance>` |
| 105 | + |
| 106 | +- Functions: |
| 107 | + - `get_fish_assets() -> Vec<FishArt>` |
| 108 | + - `update_aquarium(state: &mut AquariumState, assets: &[FishArt])` |
| 109 | + - `render_aquarium_to_string(state: &AquariumState, assets: &[FishArt]) -> String` |
| 110 | + |
| 111 | +- Widget: |
| 112 | + - `AsciiquariumWidget<'a> { state: &'a AquariumState, assets: &'a [FishArt], theme: &'a AsciiquariumTheme }` |
| 113 | + - Implements `egui::Widget` and renders a single, monospace label. |
| 114 | + |
| 115 | +## Design Notes |
| 116 | + |
| 117 | +- Stateless rendering: The widget takes immutable `&AquariumState` and `&[FishArt]` and renders a single string. No side effects, no mutation. |
| 118 | +- Parent-managed animation: The parent application updates `AquariumState` each tick using `update_aquarium`. |
| 119 | +- Float-to-int: Rendering uses `floor()` for stable projection and less jitter. |
| 120 | +- Bounds and clipping: Rendering clips safely; later fish in the slice overdraw earlier ones. |
| 121 | +- Dimensions: `AquariumState.size` is in character cells. Choose a fixed grid (e.g., 80x24) or set it based on your layout needs. |
| 122 | + |
| 123 | +## Testing |
| 124 | + |
| 125 | +Run unit tests: |
| 126 | + |
| 127 | + cargo test |
| 128 | + |
| 129 | +Tests cover: |
| 130 | +- Edge bounce behavior |
| 131 | +- Left-edge clipping in rendering |
| 132 | +- Asset measurement correctness |
| 133 | + |
| 134 | +## Tips & Troubleshooting |
| 135 | + |
| 136 | +- Misaligned ASCII: Ensure `theme.wrap` is `false` and that the container does not force wrapping. The widget uses `RichText::monospace()`. |
| 137 | +- Too small or clipped label: The rendered string’s dimensions are exactly `size.1` lines by `size.0` columns. Place it in a container large enough to display without wrapping or scaling. |
| 138 | +- Frame timing: If motion is too fast or slow, adjust fish velocities or call `update_aquarium` at your preferred tick rate. |
| 139 | + |
| 140 | +## Roadmap |
| 141 | + |
| 142 | +- Additional fish and sea creatures from the classic Asciiquarium |
| 143 | +- Configurable z-ordering and layering |
| 144 | +- Optional wrap-around movement |
| 145 | +- Simple scene randomizer utilities (spawn fish with random velocity and positions) |
| 146 | + |
| 147 | +## Contributing |
| 148 | + |
| 149 | +- Follow the Rust rules in `rust_rules.md`: |
| 150 | + - No `unwrap`/`expect` in application logic |
| 151 | + - No panics; handle errors gracefully |
| 152 | + - No unnecessary clones; prefer references |
| 153 | + - Keep modules small and focused; single responsibility |
| 154 | + - Rendering is a pure function of state |
| 155 | + - Design for theming (no hardcoded styles) |
| 156 | +- Use `rustfmt` and `clippy` with zero warnings. |
| 157 | + |
| 158 | +## License and Credits |
| 159 | + |
| 160 | +- The original Perl Asciiquarium (Kirk Baucom) materials are archived under `archive/original/`. |
| 161 | +- This crate provides an `egui`-based Rust implementation with a stateless, themeable widget design. |
| 162 | +- ASCII art in this crate is a minimal starter set for demonstration. Expand or replace as needed per your project’s licensing requirements. |
0 commit comments