diff --git a/.gitignore b/.gitignore index 7aa2768..40644e6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,11 @@ tests/fixtures/images/tiger_huge.png tests/fixtures/images/viper_4k.png tests/fixtures/images/viper_ultra_tall.png tests/fixtures/images/viper_ultra_wide.png + +# Local scratch experiments (not part of the crate) +/examples/mikey.rs +/examples/raphe.rs +/examples/truck.rs +/examples/gif_to_pack.rs +/src/image/*.jpg +/src/image/*.png diff --git a/Cargo.toml b/Cargo.toml index bb397cc..5288d1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ exclude = [ "docs/sprint-*.md", # Test assets and fixtures (large images/files) "tests/visual/", + # Depends on the excluded tests/visual/ module; would not compile from the tarball. + "tests/visual_regression.rs", "tests/assets/", "tests/fixtures/", "tests/test_assets/", @@ -292,20 +294,36 @@ name = "zone_stream" required-features = ["raytracer", "image", "chess"] [[example]] -name = "zone_stream_canvas_1" -required-features = ["raytracer", "image", "chess"] +name = "purple_rain" +required-features = ["raytracer", "image"] + +[[test]] +name = "temporal_coherence_tests" +required-features = ["image"] [[example]] -name = "zone_stream_canvas_2" -required-features = ["raytracer", "image", "chess"] +name = "generate_watermark" +required-features = ["image"] [[example]] -name = "zone_stream_canvas_3" -required-features = ["raytracer", "image", "chess"] +name = "render_braille" +required-features = ["image"] [[example]] -name = "purple_rain" -required-features = ["raytracer", "image"] +name = "render_cuda_dojo" +required-features = ["image"] + +[[example]] +name = "webcam_viewer" +required-features = ["video"] + +[[example]] +name = "webcam_selector" +required-features = ["video"] + +[[example]] +name = "webcam_tuner" +required-features = ["image", "video"] [lints.clippy] all = { level = "deny", priority = -1 } @@ -323,3 +341,30 @@ cast_lossless = "allow" # Explicit casts are clearer unnecessary_cast = "allow" # Explicit casts aid readability needless_pass_by_value = "allow" # Clearer in examples imprecise_flops = "allow" # Performance not critical in examples + +# Advisory (pedantic/nursery) lints that CI's `-D warnings` would otherwise make blocking. +# Each is allowed deliberately; `clippy::all` stays at deny and is the real quality bar. +# +# Fixing this would be a BREAKING API change: `ProgressStyle::{name,theme,describe}` are +# public trait methods returning `&str`, and the lint wants `&'static str`. Narrowing the +# trait's return type breaks every downstream implementor (see the doc example in progress/mod.rs). +unnecessary_literal_bound = "allow" +# Sibling of `imprecise_flops` (already allowed above). Rewriting `a * b + c` as `mul_add` +# fuses the multiply-add, which CHANGES floating-point rounding and can therefore shift +# rendered output (and the visual-regression baselines). It is also slower on CPUs without +# hardware FMA. Not a win for a software renderer. +suboptimal_flops = "allow" +# Graphics/math code legitimately uses short, similar names: x/y/z, cx/cy, nx/ny, r/g/b. +similar_names = "allow" +many_single_char_names = "allow" +# Packed color/bit constants (e.g. 0xRRGGBB) are more readable without digit separators. +unreadable_literal = "allow" +# Audited 2026-07-13: all 7 hits are false positives on standard formulas — the ray/sphere +# discriminant (h^2 - ac), circle tests (dx^2 + dy^2 <= r^2), and a complex quadratic map +# (z^2 = (x^2 - y^2) + i*2xy). The operand groupings are correct as written. +suspicious_operation_groupings = "allow" +# Noisy across a large rendering surface; not a correctness signal. +must_use_candidate = "allow" +missing_const_for_fn = "allow" +items_after_statements = "allow" +use_self = "allow" diff --git a/benches/braille_mapping.rs b/benches/braille_mapping.rs index 7c62fed..88b71f0 100644 --- a/benches/braille_mapping.rs +++ b/benches/braille_mapping.rs @@ -9,10 +9,11 @@ #![cfg(feature = "image")] -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; use dotmax::image::{ auto_threshold, load_from_path, pixels_to_braille, resize_to_dimensions, to_grayscale, }; +use std::hint::black_box; use std::path::Path; /// Benchmark braille mapping for standard terminal size (160×96 pixels = 80×24 cells) @@ -22,7 +23,7 @@ fn bench_pixels_to_braille_standard(c: &mut Criterion) { .expect("Failed to load sample image"); let resized = resize_to_dimensions(&img, 160, 96, true).expect("Failed to resize"); let gray = to_grayscale(&resized); - let gray_dynamic = image::DynamicImage::ImageLuma8(gray.clone()); + let gray_dynamic = image::DynamicImage::ImageLuma8(gray); let binary = auto_threshold(&gray_dynamic); c.bench_function("pixels_to_braille_160x96", |b| { @@ -39,7 +40,7 @@ fn bench_pixels_to_braille_large(c: &mut Criterion) { .expect("Failed to load sample image"); let resized = resize_to_dimensions(&img, 400, 200, true).expect("Failed to resize"); let gray = to_grayscale(&resized); - let gray_dynamic = image::DynamicImage::ImageLuma8(gray.clone()); + let gray_dynamic = image::DynamicImage::ImageLuma8(gray); let binary = auto_threshold(&gray_dynamic); c.bench_function("pixels_to_braille_400x200", |b| { @@ -56,7 +57,7 @@ fn bench_pixels_to_braille_small(c: &mut Criterion) { .expect("Failed to load sample image"); let resized = resize_to_dimensions(&img, 40, 24, true).expect("Failed to resize"); let gray = to_grayscale(&resized); - let gray_dynamic = image::DynamicImage::ImageLuma8(gray.clone()); + let gray_dynamic = image::DynamicImage::ImageLuma8(gray); let binary = auto_threshold(&gray_dynamic); c.bench_function("pixels_to_braille_40x24", |b| { diff --git a/benches/color_conversion.rs b/benches/color_conversion.rs index ae63940..73cc93c 100644 --- a/benches/color_conversion.rs +++ b/benches/color_conversion.rs @@ -6,11 +6,12 @@ //! - `rgb_to_truecolor_escape`: <50ns per conversion //! - `rgb_to_terminal_color`: <150ns per conversion (includes capability check) -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use dotmax::color::convert::{ rgb_to_ansi16, rgb_to_ansi256, rgb_to_terminal_color, rgb_to_truecolor_escape, }; use dotmax::ColorCapability; +use std::hint::black_box; /// Benchmark rgb_to_ansi256 conversion. /// @@ -20,23 +21,23 @@ fn bench_rgb_to_ansi256(c: &mut Criterion) { // Test with different color types group.bench_function("pure_red", |b| { - b.iter(|| rgb_to_ansi256(black_box(255), black_box(0), black_box(0))) + b.iter(|| rgb_to_ansi256(black_box(255), black_box(0), black_box(0))); }); group.bench_function("pure_green", |b| { - b.iter(|| rgb_to_ansi256(black_box(0), black_box(255), black_box(0))) + b.iter(|| rgb_to_ansi256(black_box(0), black_box(255), black_box(0))); }); group.bench_function("pure_blue", |b| { - b.iter(|| rgb_to_ansi256(black_box(0), black_box(0), black_box(255))) + b.iter(|| rgb_to_ansi256(black_box(0), black_box(0), black_box(255))); }); group.bench_function("gray", |b| { - b.iter(|| rgb_to_ansi256(black_box(128), black_box(128), black_box(128))) + b.iter(|| rgb_to_ansi256(black_box(128), black_box(128), black_box(128))); }); group.bench_function("random_color", |b| { - b.iter(|| rgb_to_ansi256(black_box(173), black_box(94), black_box(212))) + b.iter(|| rgb_to_ansi256(black_box(173), black_box(94), black_box(212))); }); // Batch conversion benchmark (1000 colors) @@ -49,7 +50,7 @@ fn bench_rgb_to_ansi256(c: &mut Criterion) { } } } - }) + }); }); group.finish(); @@ -62,19 +63,19 @@ fn bench_rgb_to_ansi16(c: &mut Criterion) { let mut group = c.benchmark_group("rgb_to_ansi16"); group.bench_function("pure_red", |b| { - b.iter(|| rgb_to_ansi16(black_box(255), black_box(0), black_box(0))) + b.iter(|| rgb_to_ansi16(black_box(255), black_box(0), black_box(0))); }); group.bench_function("pure_green", |b| { - b.iter(|| rgb_to_ansi16(black_box(0), black_box(255), black_box(0))) + b.iter(|| rgb_to_ansi16(black_box(0), black_box(255), black_box(0))); }); group.bench_function("gray", |b| { - b.iter(|| rgb_to_ansi16(black_box(128), black_box(128), black_box(128))) + b.iter(|| rgb_to_ansi16(black_box(128), black_box(128), black_box(128))); }); group.bench_function("random_color", |b| { - b.iter(|| rgb_to_ansi16(black_box(173), black_box(94), black_box(212))) + b.iter(|| rgb_to_ansi16(black_box(173), black_box(94), black_box(212))); }); // Batch conversion benchmark @@ -87,7 +88,7 @@ fn bench_rgb_to_ansi16(c: &mut Criterion) { } } } - }) + }); }); group.finish(); @@ -100,15 +101,15 @@ fn bench_rgb_to_truecolor_escape(c: &mut Criterion) { let mut group = c.benchmark_group("rgb_to_truecolor_escape"); group.bench_function("typical", |b| { - b.iter(|| rgb_to_truecolor_escape(black_box(255), black_box(128), black_box(0))) + b.iter(|| rgb_to_truecolor_escape(black_box(255), black_box(128), black_box(0))); }); group.bench_function("zeros", |b| { - b.iter(|| rgb_to_truecolor_escape(black_box(0), black_box(0), black_box(0))) + b.iter(|| rgb_to_truecolor_escape(black_box(0), black_box(0), black_box(0))); }); group.bench_function("max_values", |b| { - b.iter(|| rgb_to_truecolor_escape(black_box(255), black_box(255), black_box(255))) + b.iter(|| rgb_to_truecolor_escape(black_box(255), black_box(255), black_box(255))); }); group.finish(); @@ -131,7 +132,9 @@ fn bench_rgb_to_terminal_color(c: &mut Criterion) { BenchmarkId::new("capability", format!("{:?}", capability)), &capability, |b, cap| { - b.iter(|| rgb_to_terminal_color(black_box(255), black_box(128), black_box(0), *cap)) + b.iter(|| { + rgb_to_terminal_color(black_box(255), black_box(128), black_box(0), *cap) + }); }, ); } @@ -151,7 +154,7 @@ fn bench_rgb_to_terminal_color(c: &mut Criterion) { black_box(rgb_to_terminal_color(r, g, 128, cap)); } } - }) + }); }); group.finish(); @@ -174,7 +177,7 @@ fn bench_throughput(c: &mut Criterion) { )); } black_box(sum) - }) + }); }); group.bench_function("ansi16_1m_conversions", |b| { @@ -184,7 +187,7 @@ fn bench_throughput(c: &mut Criterion) { sum += u32::from(rgb_to_ansi16(black_box(173), black_box(94), black_box(212))); } black_box(sum) - }) + }); }); group.finish(); diff --git a/benches/color_rendering.rs b/benches/color_rendering.rs index 936b1b4..58c8bd5 100644 --- a/benches/color_rendering.rs +++ b/benches/color_rendering.rs @@ -13,12 +13,13 @@ #![cfg(feature = "image")] -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; use dotmax::image::color_mode::extract_cell_colors; use dotmax::image::{ load_from_path, render_image_with_color, resize_to_dimensions, ColorMode, ColorSamplingStrategy, DitheringMethod, }; +use std::hint::black_box; use std::path::Path; /// Benchmark monochrome mode (baseline, no color overhead) diff --git a/benches/color_schemes.rs b/benches/color_schemes.rs index 49c520e..d469313 100644 --- a/benches/color_schemes.rs +++ b/benches/color_schemes.rs @@ -5,18 +5,19 @@ //! Run with: //! cargo bench --bench color_schemes -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use dotmax::color::schemes::{ blue_purple, cyan_magenta, get_scheme, grayscale, green_yellow, heat_map, list_schemes, monochrome, rainbow, ColorScheme, }; +use std::hint::black_box; /// Benchmark sample() for a single scheme fn bench_sample_single(c: &mut Criterion) { let scheme = rainbow(); c.bench_function("sample_single_intensity", |b| { - b.iter(|| black_box(scheme.sample(black_box(0.5)))) + b.iter(|| black_box(scheme.sample(black_box(0.5)))); }); } @@ -30,7 +31,7 @@ fn bench_sample_gradient(c: &mut Criterion) { let intensity = i as f32 / 99.0; black_box(scheme.sample(black_box(intensity))); } - }) + }); }); } @@ -50,7 +51,7 @@ fn bench_sample_all_schemes(c: &mut Criterion) { for (name, scheme) in schemes { group.bench_with_input(BenchmarkId::new("sample", name), &scheme, |b, scheme| { - b.iter(|| black_box(scheme.sample(black_box(0.5)))) + b.iter(|| black_box(scheme.sample(black_box(0.5)))); }); } @@ -77,11 +78,11 @@ fn bench_discovery(c: &mut Criterion) { group.bench_function("list_schemes", |b| b.iter(|| black_box(list_schemes()))); group.bench_function("get_scheme_hit", |b| { - b.iter(|| black_box(get_scheme(black_box("rainbow")))) + b.iter(|| black_box(get_scheme(black_box("rainbow")))); }); group.bench_function("get_scheme_miss", |b| { - b.iter(|| black_box(get_scheme(black_box("nonexistent")))) + b.iter(|| black_box(get_scheme(black_box("nonexistent")))); }); group.finish(); @@ -99,7 +100,7 @@ fn bench_custom_scheme(c: &mut Criterion) { Color::rgb(0, 0, 255), ]; black_box(ColorScheme::new("custom", colors)) - }) + }); }); } @@ -109,19 +110,19 @@ fn bench_boundary_conditions(c: &mut Criterion) { let mut group = c.benchmark_group("boundary_conditions"); group.bench_function("sample_0.0", |b| { - b.iter(|| black_box(scheme.sample(black_box(0.0)))) + b.iter(|| black_box(scheme.sample(black_box(0.0)))); }); group.bench_function("sample_1.0", |b| { - b.iter(|| black_box(scheme.sample(black_box(1.0)))) + b.iter(|| black_box(scheme.sample(black_box(1.0)))); }); group.bench_function("sample_negative_clamped", |b| { - b.iter(|| black_box(scheme.sample(black_box(-0.5)))) + b.iter(|| black_box(scheme.sample(black_box(-0.5)))); }); group.bench_function("sample_above_1_clamped", |b| { - b.iter(|| black_box(scheme.sample(black_box(1.5)))) + b.iter(|| black_box(scheme.sample(black_box(1.5)))); }); group.finish(); diff --git a/benches/core_rendering.rs b/benches/core_rendering.rs index 98ccc18..8b0ad04 100644 --- a/benches/core_rendering.rs +++ b/benches/core_rendering.rs @@ -65,7 +65,7 @@ fn bench_grid_clear(c: &mut Criterion) { } } - group.bench_with_input(BenchmarkId::new("clear", label), &(), |b, _| { + group.bench_with_input(BenchmarkId::new("clear", label), &(), |b, ()| { b.iter(|| { grid.clear(); black_box(&grid); diff --git a/benches/density.rs b/benches/density.rs index 354273b..1ca85f6 100644 --- a/benches/density.rs +++ b/benches/density.rs @@ -4,9 +4,10 @@ //! //! Run with: `cargo bench --bench density` -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use dotmax::density::DensitySet; use dotmax::BrailleGrid; +use std::hint::black_box; /// Benchmark single intensity mapping for all predefined density sets fn bench_density_mapping(c: &mut Criterion) { @@ -181,7 +182,7 @@ fn bench_custom_density_creation(c: &mut Criterion) { let sizes = vec![5, 10, 50, 100, 256]; for size in sizes { - let chars: Vec = (0..size).map(|i| (i as u8 as char)).collect(); + let chars: Vec = (0..size).map(|i| i as u8 as char).collect(); group.bench_with_input( BenchmarkId::from_parameter(size), diff --git a/benches/dithering.rs b/benches/dithering.rs index 54058a0..e6dcbdf 100644 --- a/benches/dithering.rs +++ b/benches/dithering.rs @@ -6,9 +6,10 @@ //! - Bayer: <10ms for 160×96 images //! - Atkinson: <12ms for 160×96 images -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use dotmax::image::{apply_dithering, to_grayscale, DitheringMethod}; -use image::{DynamicImage, GrayImage, Luma}; +use criterion::{criterion_group, criterion_main, Criterion}; +use dotmax::image::{apply_dithering, DitheringMethod}; +use image::{GrayImage, Luma}; +use std::hint::black_box; /// Helper: Create a grayscale gradient image for consistent benchmarking fn create_gradient_image(width: u32, height: u32) -> GrayImage { diff --git a/benches/image_conversion.rs b/benches/image_conversion.rs index 99d60b7..bce245a 100644 --- a/benches/image_conversion.rs +++ b/benches/image_conversion.rs @@ -14,11 +14,12 @@ #![cfg(feature = "image")] -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; use dotmax::image::{ adjust_brightness, adjust_contrast, adjust_gamma, apply_threshold, auto_threshold, load_from_path, otsu_threshold, resize_to_terminal, to_grayscale, }; +use std::hint::black_box; use std::path::Path; /// Benchmark grayscale conversion for terminal-sized image (160×96) diff --git a/benches/media_detection.rs b/benches/media_detection.rs index 991fcbf..2cca9a5 100644 --- a/benches/media_detection.rs +++ b/benches/media_detection.rs @@ -2,7 +2,8 @@ //! //! Story 9.1 AC: #7 - Format detection must complete in <5ms -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; +use std::hint::black_box; use std::io::Write; use tempfile::NamedTempFile; @@ -34,19 +35,19 @@ fn bench_detect_format_from_bytes(c: &mut Criterion) { let unknown_bytes = [0x00u8; 16]; c.bench_function("detect_format_from_bytes/png", |b| { - b.iter(|| detect_format_from_bytes(black_box(&png_bytes))) + b.iter(|| detect_format_from_bytes(black_box(&png_bytes))); }); c.bench_function("detect_format_from_bytes/jpeg", |b| { - b.iter(|| detect_format_from_bytes(black_box(&jpeg_bytes))) + b.iter(|| detect_format_from_bytes(black_box(&jpeg_bytes))); }); c.bench_function("detect_format_from_bytes/gif", |b| { - b.iter(|| detect_format_from_bytes(black_box(&gif_bytes))) + b.iter(|| detect_format_from_bytes(black_box(&gif_bytes))); }); c.bench_function("detect_format_from_bytes/unknown", |b| { - b.iter(|| detect_format_from_bytes(black_box(&unknown_bytes))) + b.iter(|| detect_format_from_bytes(black_box(&unknown_bytes))); }); } @@ -77,7 +78,7 @@ fn bench_detect_format_file(c: &mut Criterion) { let path = temp_file.path(); c.bench_function("detect_format/1mb_png_file", |b| { - b.iter(|| detect_format(black_box(path))) + b.iter(|| detect_format(black_box(path))); }); } @@ -95,7 +96,7 @@ fn bench_extension_fallback(c: &mut Criterion) { let path = temp_file.path(); c.bench_function("detect_format/extension_fallback", |b| { - b.iter(|| detect_format(black_box(path))) + b.iter(|| detect_format(black_box(path))); }); } diff --git a/benches/media_playback.rs b/benches/media_playback.rs index 015e1e2..5d0394d 100644 --- a/benches/media_playback.rs +++ b/benches/media_playback.rs @@ -11,7 +11,8 @@ #![allow(clippy::items_after_statements)] #![allow(deprecated)] // criterion::black_box is deprecated but still used in existing benches -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; +use std::hint::black_box; use std::path::Path; #[cfg(feature = "image")] diff --git a/benches/primitives.rs b/benches/primitives.rs index 96af59a..16e068f 100644 --- a/benches/primitives.rs +++ b/benches/primitives.rs @@ -25,7 +25,7 @@ fn bench_line_drawing(c: &mut Criterion) { let mut group = c.benchmark_group("draw_line"); // Test different line lengths: small (10px), medium (100px), large (1000px) - for length in [10, 100, 1000].iter() { + for length in &[10, 100, 1000] { group.bench_with_input(BenchmarkId::from_parameter(length), length, |b, &length| { // Grid large enough to hold the line let mut grid = @@ -64,7 +64,7 @@ fn bench_line_octants(c: &mut Criterion) { ("shallow_positive", 0, 100, 199, 120), ]; - for (name, x0, y0, x1, y1) in test_cases.iter() { + for (name, x0, y0, x1, y1) in &test_cases { group.bench_function(*name, |b| { b.iter(|| { draw_line( @@ -90,7 +90,7 @@ fn bench_thick_line_drawing(c: &mut Criterion) { let mut grid = BrailleGrid::new(100, 100).unwrap(); // Test different thicknesses on 1000px line - for thickness in [1, 3, 5, 7, 10].iter() { + for thickness in &[1, 3, 5, 7, 10] { group.bench_with_input( BenchmarkId::from_parameter(format!("thickness_{}", thickness)), thickness, @@ -172,7 +172,7 @@ fn bench_circle_drawing(c: &mut Criterion) { let mut group = c.benchmark_group("draw_circle"); // Test different radii: small (10), medium (50), large (100), very large (500) - for radius in [10, 50, 100, 500].iter() { + for radius in &[10, 50, 100, 500] { group.bench_with_input( BenchmarkId::from_parameter(format!("radius_{}", radius)), radius, @@ -204,7 +204,7 @@ fn bench_circle_filled(c: &mut Criterion) { let mut group = c.benchmark_group("draw_circle_filled"); // Test different radii: small (10), medium (50), large (100) - for radius in [10, 50, 100].iter() { + for radius in &[10, 50, 100] { group.bench_with_input( BenchmarkId::from_parameter(format!("radius_{}", radius)), radius, @@ -237,7 +237,7 @@ fn bench_circle_thick(c: &mut Criterion) { let radius = 100; // Test different thicknesses - for thickness in [1, 3, 5, 7, 10].iter() { + for thickness in &[1, 3, 5, 7, 10] { group.bench_with_input( BenchmarkId::from_parameter(format!("thickness_{}", thickness)), thickness, diff --git a/benches/quick.rs b/benches/quick.rs index 21a9c98..fbcbcaa 100644 --- a/benches/quick.rs +++ b/benches/quick.rs @@ -8,7 +8,7 @@ //! //! Performance target: < 5ms overhead for convenience functions -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; use std::hint::black_box; // ============================================================================ @@ -47,7 +47,7 @@ fn bench_grid_overhead(c: &mut Criterion) { #[cfg(feature = "image")] mod image_benchmarks { - use super::*; + use super::{black_box, Criterion}; use dotmax::image::ImageRenderer; use dotmax::quick; use std::path::Path; diff --git a/benches/svg_rendering.rs b/benches/svg_rendering.rs index c90e4b7..58483a5 100644 --- a/benches/svg_rendering.rs +++ b/benches/svg_rendering.rs @@ -4,11 +4,12 @@ #![cfg(all(feature = "svg", feature = "image"))] -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; use dotmax::image::{ apply_dithering, auto_threshold, load_svg_from_path, pixels_to_braille, to_grayscale, DitheringMethod, }; +use std::hint::black_box; use std::path::Path; /// Benchmark small SVG rasterization (<5KB icon) diff --git a/examples/animated_gif.rs b/examples/animated_gif.rs index b04744c..ac55ea7 100644 --- a/examples/animated_gif.rs +++ b/examples/animated_gif.rs @@ -32,8 +32,7 @@ fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); let path = args .get(1) - .map(String::as_str) - .unwrap_or("tests/fixtures/media/animated.gif"); + .map_or("tests/fixtures/media/animated.gif", String::as_str); println!("Animated GIF Player - dotmax Story 9.2"); println!("=====================================\n"); diff --git a/examples/animation_buffer.rs b/examples/animation_buffer.rs index 35dd582..d8c73a9 100644 --- a/examples/animation_buffer.rs +++ b/examples/animation_buffer.rs @@ -182,7 +182,7 @@ fn main() -> Result<(), Box> { // Sleep to maintain target frame rate if frame_elapsed < target_frame_time { - std::thread::sleep(target_frame_time - frame_elapsed); + std::thread::sleep(target_frame_time.saturating_sub(frame_elapsed)); } // Print FPS to stderr every second (doesn't interfere with terminal graphics) diff --git a/examples/export_progress_catalog.rs b/examples/export_progress_catalog.rs index 8dfe026..5b2f582 100644 --- a/examples/export_progress_catalog.rs +++ b/examples/export_progress_catalog.rs @@ -16,9 +16,16 @@ use dotmax::progress::{styles_for_theme, themes, BarContext, Easing, ProgressSty use dotmax::BrailleGrid; use serde::Serialize; use std::collections::HashMap; +use std::fmt::Write as _; use std::fs; use std::path::PathBuf; +/// An exact 8-bit RGB triple, used as the palette lookup key. +type Rgb = (u8, u8, u8); + +/// A style's palette (hex strings, indexed) plus the exact-RGB -> index lookup. +type Palette = (Vec, HashMap); + const WIDTH: usize = 44; const HEIGHT: usize = 4; const FPS: u32 = 12; @@ -171,7 +178,7 @@ fn capture_style_frames(style: &dyn ProgressStyle) -> Result, dotm /// Build a ≤256-color palette for one style and a lookup from exact RGB to /// palette index. Falls back to 5-bit channel quantization, then to /// nearest-of-most-frequent if a style somehow exceeds 256 colors. -fn build_palette(frames: &[RawFrame]) -> (Vec, HashMap<(u8, u8, u8), u8>) { +fn build_palette(frames: &[RawFrame]) -> Palette { let mut counts: HashMap<(u8, u8, u8), u32> = HashMap::new(); let mut order: Vec<(u8, u8, u8)> = Vec::new(); for frame in frames { @@ -264,7 +271,9 @@ fn encode_frames(frames: &[RawFrame], map: &HashMap<(u8, u8, u8), u8>) -> Vec encoded.push_str(&format!("{index:02x}")), + Some(index) => { + let _ = write!(encoded, "{index:02x}"); + } None => encoded.push_str(".."), } } @@ -441,7 +450,7 @@ fn extract_style_source(theme_src: &str, struct_name: &str) -> Option { /// Minimal grid runtime — API-compatible subset of `dotmax::BrailleGrid` with /// identical braille bit mapping and `set_char` override semantics. -const RUNTIME_ROOT: &str = r##" +const RUNTIME_ROOT: &str = r" // =========================================================================== // Minimal runtime — a drop-in stand-in for the dotmax types the styles use. // Identical braille dot mapping and glyph-override semantics to the crate. @@ -570,11 +579,11 @@ impl BrailleGrid { Ok(()) } } -"##; +"; fn build_main(theme: &str) -> String { format!( - r##" + r#" fn main() {{ let name = std::env::args() .nth(1) @@ -627,7 +636,7 @@ fn main() {{ frame += 1; }} }} -"##, +"#, theme = theme, ) } @@ -676,7 +685,8 @@ fn build_standalone( ); let mut out = String::new(); - out.push_str(&format!( + let _ = write!( + out, "//! `{theme}` — dotmax progress styles as a standalone, dependency-free program.\n\ //!\n\ //! Generated from https://github.com/newjordan/dotmax (MIT OR Apache-2.0).\n\ @@ -688,10 +698,8 @@ fn build_standalone( //! ```sh\n\ //! rustc -O {theme}.rs && ./{theme} [style-name]\n\ //! ```\n\n" - )); - out.push_str(&format!( - "const DEFAULT_STYLE: &str = \"{default_style}\";\n" - )); + ); + let _ = writeln!(out, "const DEFAULT_STYLE: &str = \"{default_style}\";"); out.push_str(RUNTIME_ROOT); out.push_str("\npub mod progress {\n"); out.push_str(&core); diff --git a/examples/loading_bar_sheet.rs b/examples/loading_bar_sheet.rs index f931188..f8a3929 100644 --- a/examples/loading_bar_sheet.rs +++ b/examples/loading_bar_sheet.rs @@ -9,6 +9,7 @@ use dotmax::progress::{all_styles, render_lines, themes, BarContext, Easing}; use std::env; +use std::fmt::Write as _; use std::fs; use std::path::PathBuf; @@ -18,10 +19,10 @@ const FRAMES: usize = 12; const FRAME_STEP_SECS: f32 = 0.18; fn main() -> Result<(), Box> { - let output = env::args_os() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("docs/progress_loader_sheet.html")); + let output = env::args_os().nth(1).map_or_else( + || PathBuf::from("docs/progress_loader_sheet.html"), + PathBuf::from, + ); let styles = all_styles(); let mut html = String::new(); @@ -78,7 +79,7 @@ fn main() -> Result<(), Box> { html_escape_into(style.describe(), &mut html); html.push_str("\n"); html.push_str("
"); - html.push_str(&format!("{CELL_W}x{CELL_H} / {FRAMES}f")); + let _ = write!(html, "{CELL_W}x{CELL_H} / {FRAMES}f"); html.push_str("
\n"); html.push_str("\n"); } @@ -211,12 +212,13 @@ h1 { html.push_str(" \n\n\n
\n"); html.push_str("

dotmax loader cell sheet

\n"); html.push_str("
"); - html.push_str(&format!( + let _ = write!( + html, "{} styles, {} themes, {} animation frames per cell", style_count, themes().len(), FRAMES - )); + ); html.push_str("
\n
\n"); } diff --git a/examples/purple_rain.rs b/examples/purple_rain.rs index 5d12572..8a6a678 100644 --- a/examples/purple_rain.rs +++ b/examples/purple_rain.rs @@ -38,7 +38,7 @@ use std::{ }; // ───────────────────────── tiny xorshift RNG ───────────────────────── -thread_local! { static RNG: StdCell = StdCell::new(0x1234_5678); } +thread_local! { static RNG: StdCell = const { StdCell::new(0x1234_5678) }; } fn r_u32() -> u32 { RNG.with(|c| { let mut x = c.get(); @@ -63,8 +63,7 @@ fn seed_from_clock() { let nanos = Instant::now().elapsed().as_nanos() as u32 ^ std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.subsec_nanos()) - .unwrap_or(0); + .map_or(0, |d| d.subsec_nanos()); RNG.with(|c| c.set(nanos | 1)); } @@ -174,7 +173,7 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { } let vertical_split = rect.w >= rect.h; // Alternate which side the leaf sits on each step; `cw` flips polarity. - let leaf_far = ((step % 2 == 0) ^ !cw) != false; + let leaf_far = (step % 2 == 0) ^ !cw; if vertical_split { let leaf_w = ((rect.w as f32) * PHI_COMPLEMENT).round().max(3.0) as i32; @@ -185,7 +184,6 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { w: leaf_w, h: rect.h, }); - rect.w -= leaf_w; } else { out.push(Rect { x: rect.x, @@ -194,8 +192,8 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { h: rect.h, }); rect.x += leaf_w; - rect.w -= leaf_w; } + rect.w -= leaf_w; } else { let leaf_h = ((rect.h as f32) * PHI_COMPLEMENT).round().max(2.0) as i32; if leaf_far { @@ -205,7 +203,6 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { w: rect.w, h: leaf_h, }); - rect.h -= leaf_h; } else { out.push(Rect { x: rect.x, @@ -214,8 +211,8 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { h: leaf_h, }); rect.y += leaf_h; - rect.h -= leaf_h; } + rect.h -= leaf_h; } } out.push(rect); @@ -370,12 +367,9 @@ fn load_image( .render() .ok()?; let (gw, gh) = grid.dimensions(); - let mut cells: Vec> = vec![vec![' '; gw]; gh]; - for y in 0..gh { - for x in 0..gw { - cells[y][x] = grid.get_char(x, y); - } - } + let cells: Vec> = (0..gh) + .map(|y| (0..gw).map(|x| grid.get_char(x, y)).collect()) + .collect(); if luma.is_none() { luma = Some(grid.get_raw_patterns().to_vec()); } @@ -876,7 +870,6 @@ fn build_scene(w: i32, h: i32) -> Scene { } } - let _ui_rects = [ui_chess, ui_viper, ui_vhead]; // NOTE: fib zones are NOT filtered — chaos paints under everything, // and UI re-paints on top of the chaos in a final pass (see render()). @@ -897,7 +890,7 @@ fn build_scene(w: i32, h: i32) -> Scene { }; push_ui(ui_chess, Formation::ChessBoard, &mut tap_accum); // SCARY SNAKES — guaranteed visible at decent size. - let viper_idx = if assets.len() > 1 { 1 } else { 0 }; + let viper_idx = usize::from(assets.len() > 1); let vhead_idx = if assets.len() > 2 { 2 } else { viper_idx }; push_ui( ui_viper, @@ -1366,9 +1359,9 @@ fn paint_raytrace(grid: &mut [Vec], zone: &Zone, zone_id: u16) { let buf = render_with_orientation(&rt, &cam, w, h, mode, orient); let ramp: &[char] = &[' ', '·', ':', '-', '=', '+', '*', '#', '%', '@']; - for ry in 0..h { - for rx in 0..w { - let v = buf[ry][rx].clamp(0.0, 1.0); + for (ry, buf_row) in buf.iter().enumerate().take(h) { + for (rx, &raw) in buf_row.iter().enumerate().take(w) { + let v = raw.clamp(0.0, 1.0); let idx = ((v * (ramp.len() - 1) as f32).round() as usize).min(ramp.len() - 1); let ch = ramp[idx]; let i = if v > 0.30 { 0.90 } else { 0.22 }; @@ -1402,10 +1395,9 @@ fn paint_block_strata(grid: &mut [Vec], zone: &Zone, zone_id: u16) { let level_idx = match stripe { 0..=1 => 0, 2..=4 => 1, - 5..=8 => 2, - 9..=12 => 3, + 9..=12 | 16..=18 => 3, 13..=15 => 4, - 16..=18 => 3, + // 5..=8 and 19 — the shoulders of the band. _ => 2, }; let (ch, i) = levels[level_idx]; @@ -1618,15 +1610,13 @@ fn paint_cellular( let t_shift = (zone.pulse * 2.0) as i64; // Seed top row from stream bits. - let mut row = vec![false; zw]; - for rx in 0..zw { - let s = sample(stream, base + rx as i64 + t_shift) as u32; - row[rx] = (s & 1) == 1; - } + let mut row: Vec = (0..zw) + .map(|rx| (sample(stream, base + rx as i64 + t_shift) as u32) & 1 == 1) + .collect(); // Paint row, then evolve. for ry in 0..zh { - for rx in 0..zw { - let (ch, i) = if row[rx] { ('█', 0.93) } else { ('·', 0.18) }; + for (rx, &alive) in row.iter().enumerate() { + let (ch, i) = if alive { ('█', 0.93) } else { ('·', 0.18) }; put( grid, zone.rect.x + rx as i32, @@ -1734,7 +1724,7 @@ fn paint_density_grid( } /// 10. AttentionMatrix — sparse transformer-attention pattern: diagonal band, -/// a few sink columns, rare hotspots. Everything else mostly dark. +/// a few sink columns, rare hotspots. Everything else mostly dark. fn paint_attention(grid: &mut [Vec], zone: &Zone, zone_id: u16) { let color = color_for(zone.side); let zw = zone.rect.w; @@ -1920,10 +1910,10 @@ fn paint_prob_field(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &P } /// 12. ImagePanel — streams a pre-rendered braille image into the zone with -/// glitching effects. Source: dotmax's full ImageRenderer pipeline -/// (Floyd-Steinberg → Otsu → braille mapping). Each frame, a few rows get -/// scanline-torn, a sprinkle of cells get block-char corrupted, and bursts -/// of stream chars bleed through as noise. +/// glitching effects. Source: dotmax's full ImageRenderer pipeline +/// (Floyd-Steinberg → Otsu → braille mapping). Each frame, a few rows get +/// scanline-torn, a sprinkle of cells get block-char corrupted, and bursts +/// of stream chars bleed through as noise. // ─────────────── abstract dither-phase system ─────────────── // // Instead of a smooth per-cell wave, the composition is in one of two @@ -2129,10 +2119,10 @@ fn paint_image_panel( let (ch, intensity, fg_override) = if on_front { // Sweep front — sacred glyph marks the moment of transition. (sweep_front_glyph(front_kind), 1.0, Some((255, 50, 50))) - } else if h & 0x7f == 0 { + } else if h.trailing_zeros() >= 7 { let blocks: &[char] = &['█', '▓', '▒', '░']; (blocks[(h as usize >> 7) % blocks.len()], 1.0, None) - } else if h & 0x3f == 0 { + } else if h.trailing_zeros() >= 6 { let sc = sample(ctx.stream, base_stream + (ry as i64) * 7 + rx as i64); (sc, 0.85, None) } else if img_ch == '\u{2800}' { @@ -2183,9 +2173,9 @@ fn paint_image_panel( } /// 13. RaytraceCube — wireframe cube on BLACK background. Built from 8 -/// corners + 12 edges, rotated via raytracer's yaw/pitch helper. Lines -/// rasterized with Bresenham. Zero mask, zero pipes — pure geometry in -/// the middle of the chaos. +/// corners + 12 edges, rotated via raytracer's yaw/pitch helper. Lines +/// rasterized with Bresenham. Zero mask, zero pipes — pure geometry in +/// the middle of the chaos. fn paint_raytrace_cube(grid: &mut [Vec], zone: &Zone, zone_id: u16) { let zw = zone.rect.w; let zh = zone.rect.h; @@ -2378,7 +2368,7 @@ fn paint_formation(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &Pa Formation::RegisterDump => paint_register_dump(grid, zone, zone_id, ctx.stream, ctx.cursor), Formation::TextmarkConverter => paint_textmark(grid, zone, zone_id, ctx.stream, ctx.cursor), Formation::Cellular1D { rule } => { - paint_cellular(grid, zone, zone_id, rule, ctx.stream, ctx.cursor) + paint_cellular(grid, zone, zone_id, rule, ctx.stream, ctx.cursor); } Formation::Marquee => paint_marquee(grid, zone, zone_id, ctx.stream, ctx.cursor), Formation::DensityGrid => paint_density_grid(grid, zone, zone_id, ctx.stream, ctx.cursor), @@ -2442,7 +2432,7 @@ fn apply_transform(c: char, t: Transform) -> char { Transform::BitRev => { if c.is_ascii() { let mut b = c as u8; - b = (b >> 4) | (b << 4); + b = b.rotate_left(4); b = ((b >> 2) & 0x33) | ((b << 2) & 0xcc); b = ((b >> 1) & 0x55) | ((b << 1) & 0xaa); if (b as char).is_ascii_graphic() { @@ -2799,8 +2789,8 @@ fn paint_glitch_insertion( let vh = variant.h as i32; // crop.x/.y is the source offset where this insertion's (0,0) maps to. // Native 1:1 sampling — image scrolls/crops, never warps. - let off_x = ins.crop.map(|c| c.x).unwrap_or(0); - let off_y = ins.crop.map(|c| c.y).unwrap_or(0); + let off_x = ins.crop.map_or(0, |c| c.x); + let off_y = ins.crop.map_or(0, |c| c.y); let age = (cursor - ins.spawn_cursor) / ins.duration_chars.max(0.01); let life_factor = if age < 0.15 { @@ -3094,7 +3084,7 @@ fn render(scene: &Scene) -> Vec> { // 7) Flip: mirror every row horizontally at the very end so the creed // flips too — the mirror universe has its own scripture. if scene.flipped { - for row in grid.iter_mut() { + for row in &mut grid { row.reverse(); } } @@ -3161,16 +3151,15 @@ fn main() -> io::Result<()> { if let Event::Key(k) = event::read()? { match (k.code, k.modifiers) { // Always-on quit - (KeyCode::Esc, _) => break, + (KeyCode::Esc | KeyCode::Char('q'), _) => break, (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => break, // Toggles moved to Ctrl-modified so plain f/r are typeable. (KeyCode::Char('f'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.flipped = !scene.flipped + scene.flipped = !scene.flipped; } (KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.reversed = !scene.reversed + scene.reversed = !scene.reversed; } - (KeyCode::Char('q'), _) => break, _ => {} } } @@ -3185,7 +3174,7 @@ fn main() -> io::Result<()> { let elapsed = last.elapsed(); if elapsed < target { - std::thread::sleep(target - elapsed); + std::thread::sleep(target.saturating_sub(elapsed)); } } Ok(()) diff --git a/examples/render_braille.rs b/examples/render_braille.rs index e62506c..353d260 100644 --- a/examples/render_braille.rs +++ b/examples/render_braille.rs @@ -7,6 +7,7 @@ //! frame. use dotmax::image::{DitheringMethod, ImageRenderer}; +use std::fmt::Write as _; use std::fs; use std::path::{Path, PathBuf}; @@ -26,7 +27,7 @@ fn env_f32(key: &str, default: f32) -> f32 { fn env_u8(key: &str, default: u8) -> Option { match std::env::var(key).ok().as_deref() { - Some("auto") | Some("AUTO") | Some("") => None, + Some("auto" | "AUTO" | "") => None, Some(s) => s.parse().ok(), None => Some(default), } @@ -47,7 +48,7 @@ fn env_dither(key: &str) -> DitheringMethod { .map(str::to_lowercase) .as_deref() { - Some("floyd") | Some("floydsteinberg") => DitheringMethod::FloydSteinberg, + Some("floyd" | "floydsteinberg") => DitheringMethod::FloydSteinberg, Some("bayer") => DitheringMethod::Bayer, Some("atkinson") => DitheringMethod::Atkinson, _ => DitheringMethod::None, @@ -163,7 +164,7 @@ fn main() -> Result<(), Box> { // Flip the 8 dot bits of each braille glyph (0x2800 + bits) // so filled ↔ empty — useful when dithering gave a solid // background. - for row in unicode.iter_mut() { + for row in &mut unicode { for ch in row.iter_mut() { let bits = (*ch as u32).saturating_sub(0x2800) as u8; *ch = char::from_u32(0x2800 + (!bits as u32)).unwrap_or(*ch); @@ -188,10 +189,10 @@ fn main() -> Result<(), Box> { let svg_h = grid.height() as f32 * row_h; let mut svg = String::with_capacity(4096); - svg.push_str(&format!( - "\n", - svg_w, svg_h, svg_w, svg_h, font_px - )); + let _ = writeln!( + svg, + "" + ); svg.push_str("\n"); for (row_idx, row) in unicode.iter().enumerate() { let line: String = row.iter().collect(); @@ -200,10 +201,10 @@ fn main() -> Result<(), Box> { .replace('&', "&") .replace('<', "<") .replace('>', ">"); - svg.push_str(&format!( - "{}\n", - y, svg_w, escaped - )); + let _ = writeln!( + svg, + "{escaped}" + ); } svg.push_str("\n"); diff --git a/examples/render_cuda_dojo.rs b/examples/render_cuda_dojo.rs index 3449f15..a983589 100644 --- a/examples/render_cuda_dojo.rs +++ b/examples/render_cuda_dojo.rs @@ -3,6 +3,7 @@ //! Settings are tunable via environment variables — see render_cuda_dojo.sh. use dotmax::image::{DitheringMethod, ImageRenderer}; +use std::fmt::Write as _; use std::fs; use std::path::PathBuf; @@ -15,7 +16,7 @@ fn env_f32(key: &str, default: f32) -> f32 { fn env_u8(key: &str, default: u8) -> Option { match std::env::var(key).ok().as_deref() { - Some("auto") | Some("AUTO") | Some("") => None, + Some("auto" | "AUTO" | "") => None, Some(s) => s.parse().ok(), None => Some(default), } @@ -36,7 +37,7 @@ fn env_dither(key: &str) -> DitheringMethod { .map(str::to_lowercase) .as_deref() { - Some("floyd") | Some("floydsteinberg") => DitheringMethod::FloydSteinberg, + Some("floyd" | "floydsteinberg") => DitheringMethod::FloydSteinberg, Some("bayer") => DitheringMethod::Bayer, Some("atkinson") => DitheringMethod::Atkinson, _ => DitheringMethod::None, @@ -133,10 +134,10 @@ fn main() -> Result<(), Box> { let svg_h = grid.height() as f32 * row_h; let mut svg = String::with_capacity(4096); - svg.push_str(&format!( - "\n", - svg_w, svg_h, svg_w, svg_h, font_px - )); + let _ = writeln!( + svg, + "" + ); svg.push_str("\n"); for (row_idx, row) in unicode.iter().enumerate() { let line: String = row.iter().collect(); @@ -146,10 +147,10 @@ fn main() -> Result<(), Box> { .replace('&', "&") .replace('<', "<") .replace('>', ">"); - svg.push_str(&format!( - "{}\n", - y, svg_w, escaped - )); + let _ = writeln!( + svg, + "{escaped}" + ); } svg.push_str("\n"); diff --git a/examples/render_tuner.rs b/examples/render_tuner.rs index c5fbca2..e39eb9c 100644 --- a/examples/render_tuner.rs +++ b/examples/render_tuner.rs @@ -48,6 +48,7 @@ use std::time::{Duration, Instant}; use crossterm::event::{self, Event, KeyCode}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::{cursor, execute}; +use std::fmt::Write as _; use std::io::{stdout, Write}; /// Current render settings being tuned. @@ -193,7 +194,7 @@ fn render_grid_line(grid: &BrailleGrid, y: usize, max_width: usize) -> String { #[inline] fn flush_color_batch(output: &mut String, chars: &[char], color: Option) { if let Some(c) = color { - output.push_str(&format!("\x1b[38;2;{};{};{}m", c.r, c.g, c.b)); + let _ = write!(output, "\x1b[38;2;{};{};{}m", c.r, c.g, c.b); } output.extend(chars.iter()); } @@ -409,7 +410,7 @@ fn load_media(path: &Path) -> dotmax::Result<(MediaSource, bool)> { let ext = path .extension() .and_then(|e| e.to_str()) - .map(|s| s.to_lowercase()) + .map(str::to_lowercase) .unwrap_or_default(); // Check if it's a video format @@ -455,11 +456,11 @@ fn run_image_tuner(img: image::DynamicImage, file_path: &str) -> dotmax::Result< if let Event::Key(key) = event::read()? { match key.code { KeyCode::Char('q') | KeyCode::Esc => break, - KeyCode::Char('d') | KeyCode::Char('D') => { + KeyCode::Char('d' | 'D') => { settings.cycle_dithering(); needs_redraw = true; } - KeyCode::Char('t') | KeyCode::Char('T') => { + KeyCode::Char('t' | 'T') => { settings.toggle_threshold_mode(); needs_redraw = true; } @@ -495,15 +496,15 @@ fn run_image_tuner(img: image::DynamicImage, file_path: &str) -> dotmax::Result< settings.adjust_gamma(-0.1); needs_redraw = true; } - KeyCode::Char('m') | KeyCode::Char('M') => { + KeyCode::Char('m' | 'M') => { settings.cycle_color_mode(); needs_redraw = true; } - KeyCode::Char('r') | KeyCode::Char('R') => { + KeyCode::Char('r' | 'R') => { settings = TunerSettings::default(); needs_redraw = true; } - KeyCode::Char('s') | KeyCode::Char('S') => { + KeyCode::Char('s' | 'S') => { settings.show_snippet = !settings.show_snippet; needs_redraw = true; } @@ -616,12 +617,12 @@ fn run_video_tuner(video_path: &str) -> dotmax::Result<()> { settings.paused = !settings.paused; hud_dirty = true; } - KeyCode::Char('d') | KeyCode::Char('D') => { + KeyCode::Char('d' | 'D') => { settings.cycle_dithering(); player.set_dithering(settings.dithering); hud_dirty = true; } - KeyCode::Char('t') | KeyCode::Char('T') => { + KeyCode::Char('t' | 'T') => { settings.toggle_threshold_mode(); player.set_threshold(settings.threshold); hud_dirty = true; @@ -666,18 +667,18 @@ fn run_video_tuner(video_path: &str) -> dotmax::Result<()> { player.set_gamma(settings.gamma); hud_dirty = true; } - KeyCode::Char('m') | KeyCode::Char('M') => { + KeyCode::Char('m' | 'M') => { settings.cycle_color_mode(); player.set_color_mode(settings.color_mode); hud_dirty = true; } - KeyCode::Char('r') | KeyCode::Char('R') => { + KeyCode::Char('r' | 'R') => { settings = TunerSettings::default(); player = create_video_player(video_path, &settings)?; frame_buffer = FrameBuffer::new(); // Reset buffer on player reset hud_dirty = true; } - KeyCode::Char('s') | KeyCode::Char('S') => { + KeyCode::Char('s' | 'S') => { settings.show_snippet = !settings.show_snippet; hud_dirty = true; } @@ -736,7 +737,7 @@ fn run_video_tuner(video_path: &str) -> dotmax::Result<()> { // Wait for frame timing if render_time < delay { - std::thread::sleep(delay - render_time); + std::thread::sleep(delay.saturating_sub(render_time)); } } Some(Err(e)) => return Err(e), @@ -811,12 +812,12 @@ fn draw_video_frame_optimized( for (y, line_content) in changed_lines { // Move cursor to line position and write the pre-rendered line - output.push_str(&format!("\x1b[{};1H{}", y + 1, line_content)); + let _ = write!(output, "\x1b[{};1H{}", y + 1, line_content); } // Position cursor for HUD (always at bottom) let hud_start_row = params.term_height.saturating_sub(params.hud_height); - output.push_str(&format!("\x1b[{};1H", hud_start_row + 1)); + let _ = write!(output, "\x1b[{};1H", hud_start_row + 1); // Write frame content in one syscall write!(stdout, "{}", output)?; @@ -884,8 +885,7 @@ fn draw_hud_optimized( settings.dithering_name(), settings .threshold - .map(|t| format!("{}", t)) - .unwrap_or_else(|| "Auto".to_string()), + .map_or_else(|| "Auto".to_string(), |t| t.to_string()), fps, render_fps, // Show render FPS in parentheses ); @@ -966,8 +966,7 @@ fn draw_hud( settings.dithering_name(), settings .threshold - .map(|t| format!("{}", t)) - .unwrap_or_else(|| "Auto".to_string()), + .map_or_else(|| "Auto".to_string(), |t| t.to_string()), fps ) } else { @@ -976,8 +975,7 @@ fn draw_hud( settings.dithering_name(), settings .threshold - .map(|t| format!("{}", t)) - .unwrap_or_else(|| "Auto".to_string()), + .map_or_else(|| "Auto".to_string(), |t| t.to_string()), render_time.as_secs_f64() * 1000.0 ) }; diff --git a/examples/universal_media.rs b/examples/universal_media.rs index 9d21815..31ee0d5 100644 --- a/examples/universal_media.rs +++ b/examples/universal_media.rs @@ -29,6 +29,7 @@ use dotmax::media::{detect_format, detect_format_from_bytes, MediaContent, MediaFormat}; use dotmax::quick; use std::env; +use std::fmt::Write as _; use std::io::Read; use std::path::Path; @@ -172,11 +173,10 @@ fn demonstrate_format_detection() { for (name, bytes) in test_cases { let format = detect_format_from_bytes(&bytes); - let hex: String = bytes - .iter() - .take(8) - .map(|b| format!("{:02X} ", b)) - .collect(); + let mut hex = String::new(); + for b in bytes.iter().take(8) { + let _ = write!(hex, "{b:02X} "); + } println!("{:12} | {} | -> {}", name, hex.trim(), format); } diff --git a/examples/video_player.rs b/examples/video_player.rs index 9a15e74..49560cc 100644 --- a/examples/video_player.rs +++ b/examples/video_player.rs @@ -133,7 +133,7 @@ fn parse_args() -> Result { } _ => { if opts.video_path.is_empty() { - opts.video_path = args[i].clone(); + opts.video_path.clone_from(&args[i]); } else { return Err(format!("Unexpected argument: {}", args[i])); } @@ -213,8 +213,7 @@ fn main() -> dotmax::Result<()> { println!( " Threshold: {}", opts.threshold - .map(|t| t.to_string()) - .unwrap_or_else(|| "Auto (Otsu)".to_string()) + .map_or_else(|| "Auto (Otsu)".to_string(), |t| t.to_string()) ); println!(" Brightness: {:.2}", opts.brightness); println!(" Contrast: {:.2}", opts.contrast); diff --git a/examples/webcam_tuner.rs b/examples/webcam_tuner.rs index 6b0c5ed..9f62414 100644 --- a/examples/webcam_tuner.rs +++ b/examples/webcam_tuner.rs @@ -328,7 +328,7 @@ fn run_webcam_tuner(camera_index: usize) -> dotmax::Result<()> { player.height(), player.fps() ); - std::thread::sleep(Duration::from_millis(1000)); + std::thread::sleep(Duration::from_secs(1)); // Enter raw mode and alternate screen terminal::enable_raw_mode()?; diff --git a/examples/zone_stream.rs b/examples/zone_stream.rs index 68b4f4c..1c2e273 100644 --- a/examples/zone_stream.rs +++ b/examples/zone_stream.rs @@ -38,7 +38,7 @@ use std::{ }; // ───────────────────────── tiny xorshift RNG ───────────────────────── -thread_local! { static RNG: StdCell = StdCell::new(0x1234_5678); } +thread_local! { static RNG: StdCell = const { StdCell::new(0x1234_5678) }; } fn r_u32() -> u32 { RNG.with(|c| { let mut x = c.get(); @@ -63,8 +63,7 @@ fn seed_from_clock() { let nanos = Instant::now().elapsed().as_nanos() as u32 ^ std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.subsec_nanos()) - .unwrap_or(0); + .map_or(0, |d| d.subsec_nanos()); RNG.with(|c| c.set(nanos | 1)); } @@ -174,7 +173,7 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { } let vertical_split = rect.w >= rect.h; // Alternate which side the leaf sits on each step; `cw` flips polarity. - let leaf_far = ((step % 2 == 0) ^ !cw) != false; + let leaf_far = (step % 2 == 0) ^ !cw; if vertical_split { let leaf_w = ((rect.w as f32) * PHI_COMPLEMENT).round().max(3.0) as i32; @@ -185,7 +184,6 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { w: leaf_w, h: rect.h, }); - rect.w -= leaf_w; } else { out.push(Rect { x: rect.x, @@ -194,8 +192,8 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { h: rect.h, }); rect.x += leaf_w; - rect.w -= leaf_w; } + rect.w -= leaf_w; } else { let leaf_h = ((rect.h as f32) * PHI_COMPLEMENT).round().max(2.0) as i32; if leaf_far { @@ -205,7 +203,6 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { w: rect.w, h: leaf_h, }); - rect.h -= leaf_h; } else { out.push(Rect { x: rect.x, @@ -214,8 +211,8 @@ fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { h: leaf_h, }); rect.y += leaf_h; - rect.h -= leaf_h; } + rect.h -= leaf_h; } } out.push(rect); @@ -370,12 +367,9 @@ fn load_image( .render() .ok()?; let (gw, gh) = grid.dimensions(); - let mut cells: Vec> = vec![vec![' '; gw]; gh]; - for y in 0..gh { - for x in 0..gw { - cells[y][x] = grid.get_char(x, y); - } - } + let cells: Vec> = (0..gh) + .map(|y| (0..gw).map(|x| grid.get_char(x, y)).collect()) + .collect(); if luma.is_none() { luma = Some(grid.get_raw_patterns().to_vec()); } @@ -790,9 +784,6 @@ fn build_scene(w: i32, h: i32) -> Scene { } } - let _ui_rects = [ - ui_atm, ui_agent_a, ui_agent_b, ui_chess, ui_payout, ui_term, ui_viper, ui_vhead, - ]; // NOTE: fib zones are NOT filtered — chaos paints under everything, // and UI re-paints on top of the chaos in a final pass (see render()). @@ -826,7 +817,7 @@ fn build_scene(w: i32, h: i32) -> Scene { push_ui(ui_payout, Formation::PayoutButton, &mut tap_accum); push_ui(ui_term, Formation::TerminalInput, &mut tap_accum); // SCARY SNAKES — guaranteed visible at decent size. - let viper_idx = if assets.len() > 1 { 1 } else { 0 }; + let viper_idx = usize::from(assets.len() > 1); let vhead_idx = if assets.len() > 2 { 2 } else { viper_idx }; push_ui( ui_viper, @@ -1307,9 +1298,9 @@ fn paint_raytrace(grid: &mut [Vec], zone: &Zone, zone_id: u16) { let buf = render_with_orientation(&rt, &cam, w, h, mode, orient); let ramp: &[char] = &[' ', '·', ':', '-', '=', '+', '*', '#', '%', '@']; - for ry in 0..h { - for rx in 0..w { - let v = buf[ry][rx].clamp(0.0, 1.0); + for (ry, row) in buf.iter().enumerate().take(h) { + for (rx, &depth) in row.iter().enumerate().take(w) { + let v = depth.clamp(0.0, 1.0); let idx = ((v * (ramp.len() - 1) as f32).round() as usize).min(ramp.len() - 1); let ch = ramp[idx]; let i = if v > 0.30 { 0.90 } else { 0.22 }; @@ -1340,13 +1331,12 @@ fn paint_block_strata(grid: &mut [Vec], zone: &Zone, zone_id: u16) { // Step-function over y: 20-row cycle with custom profile. for ry in 0..zone.rect.h { let stripe = (ry + scroll).rem_euclid(20); + // Bands 5..=8 and 19 both land on level 2, so they share the wildcard arm. let level_idx = match stripe { 0..=1 => 0, 2..=4 => 1, - 5..=8 => 2, - 9..=12 => 3, + 9..=12 | 16..=18 => 3, 13..=15 => 4, - 16..=18 => 3, _ => 2, }; let (ch, i) = levels[level_idx]; @@ -1559,15 +1549,16 @@ fn paint_cellular( let t_shift = (zone.pulse * 2.0) as i64; // Seed top row from stream bits. - let mut row = vec![false; zw]; - for rx in 0..zw { - let s = sample(stream, base + rx as i64 + t_shift) as u32; - row[rx] = (s & 1) == 1; - } + let mut row: Vec = (0..zw) + .map(|rx| { + let s = sample(stream, base + rx as i64 + t_shift) as u32; + (s & 1) == 1 + }) + .collect(); // Paint row, then evolve. for ry in 0..zh { - for rx in 0..zw { - let (ch, i) = if row[rx] { ('█', 0.93) } else { ('·', 0.18) }; + for (rx, &alive) in row.iter().enumerate() { + let (ch, i) = if alive { ('█', 0.93) } else { ('·', 0.18) }; put( grid, zone.rect.x + rx as i32, @@ -1675,7 +1666,7 @@ fn paint_density_grid( } /// 10. AttentionMatrix — sparse transformer-attention pattern: diagonal band, -/// a few sink columns, rare hotspots. Everything else mostly dark. +/// a few sink columns, rare hotspots. Everything else mostly dark. fn paint_attention(grid: &mut [Vec], zone: &Zone, zone_id: u16) { let color = color_for(zone.side); let zw = zone.rect.w; @@ -1861,10 +1852,10 @@ fn paint_prob_field(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &P } /// 12. ImagePanel — streams a pre-rendered braille image into the zone with -/// glitching effects. Source: dotmax's full ImageRenderer pipeline -/// (Floyd-Steinberg → Otsu → braille mapping). Each frame, a few rows get -/// scanline-torn, a sprinkle of cells get block-char corrupted, and bursts -/// of stream chars bleed through as noise. +/// glitching effects. Source: dotmax's full ImageRenderer pipeline +/// (Floyd-Steinberg → Otsu → braille mapping). Each frame, a few rows get +/// scanline-torn, a sprinkle of cells get block-char corrupted, and bursts +/// of stream chars bleed through as noise. // ─────────────── abstract dither-phase system ─────────────── // // Instead of a smooth per-cell wave, the composition is in one of two @@ -2058,10 +2049,10 @@ fn paint_image_panel( let (ch, intensity, fg_override) = if on_front { // Sweep front — sacred glyph marks the moment of transition. (sweep_front_glyph(front_kind), 1.0, Some((255, 50, 50))) - } else if h & 0x7f == 0 { + } else if h.trailing_zeros() >= 7 { let blocks: &[char] = &['█', '▓', '▒', '░']; (blocks[(h as usize >> 7) % blocks.len()], 1.0, None) - } else if h & 0x3f == 0 { + } else if h.trailing_zeros() >= 6 { let sc = sample(ctx.stream, base_stream + (ry as i64) * 7 + rx as i64); (sc, 0.85, None) } else if img_ch == '\u{2800}' { @@ -2112,9 +2103,9 @@ fn paint_image_panel( } /// 13. RaytraceCube — wireframe cube on BLACK background. Built from 8 -/// corners + 12 edges, rotated via raytracer's yaw/pitch helper. Lines -/// rasterized with Bresenham. Zero mask, zero pipes — pure geometry in -/// the middle of the chaos. +/// corners + 12 edges, rotated via raytracer's yaw/pitch helper. Lines +/// rasterized with Bresenham. Zero mask, zero pipes — pure geometry in +/// the middle of the chaos. fn paint_raytrace_cube(grid: &mut [Vec], zone: &Zone, zone_id: u16) { let zw = zone.rect.w; let zh = zone.rect.h; @@ -2318,6 +2309,9 @@ fn paint_box_border(grid: &mut [Vec], zone: &Zone, zone_id: u16, intensi ); } +// 8 args = `put`'s 7 plus a right-hand clip bound. Bundling them into a struct would +// churn every one of the ~9 UI call sites for no readability gain in a demo. +#[allow(clippy::too_many_arguments)] fn put_str( grid: &mut [Vec], x: i32, @@ -2328,13 +2322,11 @@ fn put_str( oid: u16, max_x: i32, ) { - let mut cx = x; - for ch in s.chars() { + for (cx, ch) in (x..).zip(s.chars()) { if cx >= max_x { break; } put(grid, cx, y, ch, color, i, oid); - cx += 1; } } @@ -2617,7 +2609,7 @@ fn paint_formation(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &Pa Formation::RegisterDump => paint_register_dump(grid, zone, zone_id, ctx.stream, ctx.cursor), Formation::TextmarkConverter => paint_textmark(grid, zone, zone_id, ctx.stream, ctx.cursor), Formation::Cellular1D { rule } => { - paint_cellular(grid, zone, zone_id, rule, ctx.stream, ctx.cursor) + paint_cellular(grid, zone, zone_id, rule, ctx.stream, ctx.cursor); } Formation::Marquee => paint_marquee(grid, zone, zone_id, ctx.stream, ctx.cursor), Formation::DensityGrid => paint_density_grid(grid, zone, zone_id, ctx.stream, ctx.cursor), @@ -2627,12 +2619,12 @@ fn paint_formation(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &Pa Formation::RaytraceCube => paint_raytrace_cube(grid, zone, zone_id), Formation::Atm => paint_atm(grid, zone, zone_id, ctx.balance, ctx.cursor), Formation::AgentSlot { player } => { - paint_agent_slot(grid, zone, zone_id, player, ctx.input_buffer) + paint_agent_slot(grid, zone, zone_id, player, ctx.input_buffer); } Formation::ChessBoard => paint_chess_board(grid, zone, zone_id, ctx.chess_pos, ctx.cursor), Formation::PayoutButton => paint_payout_button(grid, zone, zone_id, ctx.cursor), Formation::TerminalInput => { - paint_terminal_input(grid, zone, zone_id, ctx.input_buffer, ctx.cursor) + paint_terminal_input(grid, zone, zone_id, ctx.input_buffer, ctx.cursor); } } } @@ -2689,7 +2681,7 @@ fn apply_transform(c: char, t: Transform) -> char { Transform::BitRev => { if c.is_ascii() { let mut b = c as u8; - b = (b >> 4) | (b << 4); + b = b.rotate_left(4); b = ((b >> 2) & 0x33) | ((b << 2) & 0xcc); b = ((b >> 1) & 0x55) | ((b << 1) & 0xaa); if (b as char).is_ascii_graphic() { @@ -3046,8 +3038,8 @@ fn paint_glitch_insertion( let vh = variant.h as i32; // crop.x/.y is the source offset where this insertion's (0,0) maps to. // Native 1:1 sampling — image scrolls/crops, never warps. - let off_x = ins.crop.map(|c| c.x).unwrap_or(0); - let off_y = ins.crop.map(|c| c.y).unwrap_or(0); + let off_x = ins.crop.map_or(0, |c| c.x); + let off_y = ins.crop.map_or(0, |c| c.y); let age = (cursor - ins.spawn_cursor) / ins.duration_chars.max(0.01); let life_factor = if age < 0.15 { @@ -3350,7 +3342,7 @@ fn render(scene: &Scene) -> Vec> { // 7) Flip: mirror every row horizontally at the very end so the creed // flips too — the mirror universe has its own scripture. if scene.flipped { - for row in grid.iter_mut() { + for row in &mut grid { row.reverse(); } } @@ -3421,10 +3413,10 @@ fn main() -> io::Result<()> { (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => break, // Toggles moved to Ctrl-modified so plain f/r are typeable. (KeyCode::Char('f'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.flipped = !scene.flipped + scene.flipped = !scene.flipped; } (KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.reversed = !scene.reversed + scene.reversed = !scene.reversed; } // Backspace edits the live input buffer. (KeyCode::Backspace, _) => { @@ -3435,10 +3427,11 @@ fn main() -> io::Result<()> { scene.input_buffer.clear(); } // Plain printable chars (no Ctrl) → input buffer. - (KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { - if scene.input_buffer.chars().count() < 30 { - scene.input_buffer.push(c); - } + (KeyCode::Char(c), m) + if !m.contains(KeyModifiers::CONTROL) + && scene.input_buffer.chars().count() < 30 => + { + scene.input_buffer.push(c); } _ => {} } @@ -3454,7 +3447,7 @@ fn main() -> io::Result<()> { let elapsed = last.elapsed(); if elapsed < target { - std::thread::sleep(target - elapsed); + std::thread::sleep(target.checked_sub(elapsed).unwrap()); } } Ok(()) diff --git a/examples/zone_stream_canvas_1.rs b/examples/zone_stream_canvas_1.rs deleted file mode 100644 index 68b4f4c..0000000 --- a/examples/zone_stream_canvas_1.rs +++ /dev/null @@ -1,3466 +0,0 @@ -//! ─── creed of the hose ─── -//! -//! One cursor flows; all cells awaken. -//! At the shared edge, the data bleeds. -//! φ = 1.618 is the architect. 2π · (1 − 1/φ) is the pitch. -//! The cube watches from the east. It keeps count. -//! -//! Every frame, three truths are sung together: -//! formations hold their ground, -//! pipes carry what cannot be held, -//! the sweep-front paints the new in. -//! -//! Press f to invert the world. Press r to unwind it. -//! Press q / Esc to leave the room. -//! -//! Run: cargo run --example zone_stream --release --features "raytracer image" - -use crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyModifiers}, - execute, queue, - style::{Color, Print, ResetColor, SetForegroundColor}, - terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use dotmax::chess::board::{render_position_with_options, RenderOptions}; -use dotmax::image::{DitheringMethod, ImageRenderer}; -use dotmax::raytracer::wireframe::rotate_vec_yaw_pitch_roll; -use dotmax::raytracer::{ - render_with_orientation, Camera, RenderMode, Scene as RtScene, Sphere, Vector3, - WireframeRotation, -}; -use shakmaty::{Chess, Position}; -use std::{ - cell::Cell as StdCell, - io::{self, Write}, - path::Path, - time::{Duration, Instant}, -}; - -// ───────────────────────── tiny xorshift RNG ───────────────────────── -thread_local! { static RNG: StdCell = StdCell::new(0x1234_5678); } -fn r_u32() -> u32 { - RNG.with(|c| { - let mut x = c.get(); - if x == 0 { - x = 0x9E37_79B9; - } - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - c.set(x); - x - }) -} -fn r_f32() -> f32 { - (r_u32() as f32) / (u32::MAX as f32) -} -fn r_pick(xs: &[T]) -> T { - xs[(r_u32() as usize) % xs.len()] -} - -fn seed_from_clock() { - let nanos = Instant::now().elapsed().as_nanos() as u32 - ^ std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.subsec_nanos()) - .unwrap_or(0); - RNG.with(|c| c.set(nanos | 1)); -} - -// ───────────────────────── glyph pools ───────────────────────── -const HEX: &[char] = &[ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', -]; -const BITS: &[char] = &['0', '1']; -const PUNCT: &[char] = &[ - '!', '@', '#', '$', '%', '&', '*', '+', '=', '<', '>', '?', '/', '\\', '^', '~', -]; -const KANA: &[char] = &[ - 'ヲ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ッ', 'ア', 'イ', 'ウ', 'エ', 'オ', 'ハ', 'ヒ', 'フ', 'ヘ', - 'ホ', 'マ', 'ミ', 'ム', -]; -const BLOCK: &[char] = &['░', '▒', '▓', '█', '▚', '▞', '▙', '▟']; -const GREEK: &[char] = &[ - 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'λ', 'μ', 'π', 'σ', 'τ', 'φ', 'ψ', 'ω', -]; - -const SNIPPETS: &[&str] = &[ - " ::SYNC:: ", - " 0xDEAD ", - " [OK] ", - " ROUTINE 0x42 ", - " ACK ", - " FAULT ", - " λ=0x1F ", - " >>> ", - " <<< ", - " /proc/self ", - " alloc= ", - " ACK 0x7F ", - " EOF ", - " NULL ", - " TX/RX ", - " PID:4821 ", - " SIG 0x4A ", - " ENTER ", - " φ=1.618 ", - " √2=1.414 ", - " θ=π/φ ", - " FIB(13)=233 ", - " // BREACH ", - // scripture - " ✦ INVOCATION ✦ ", - " ∴ by golden angle ∴ ", - " one cursor many cells ", - " ☸ hose is holy ☸ ", - " the cube watches ", - " as above so below ", - " // GOLDEN HOUR // ", - " ⚘ signal becomes sacrament ⚘ ", - " ∞ one cursor ∞ ", - " ACK the geometry ", - " fold by fold ", - " ✧ enter be transformed ✧ ", - " ∇ scripture ∇ ", -]; - -// ───────────────────────── stream source ───────────────────────── -fn build_stream(len: usize) -> Vec { - let pools: &[&[char]] = &[HEX, BITS, PUNCT, KANA, BLOCK, GREEK]; - let mut out = Vec::with_capacity(len); - while out.len() < len { - if r_f32() < 0.15 { - let s = SNIPPETS[(r_u32() as usize) % SNIPPETS.len()]; - for ch in s.chars() { - if out.len() >= len { - break; - } - out.push(ch); - } - } else { - let p = pools[(r_u32() as usize) % pools.len()]; - let burst = 4 + (r_u32() as usize) % 9; - for _ in 0..burst { - if out.len() >= len { - break; - } - out.push(r_pick(p)); - } - } - } - out -} - -// ───────────────────────── geometry ───────────────────────── -#[derive(Clone, Copy, Debug)] -struct Rect { - x: i32, - y: i32, - w: i32, - h: i32, -} - -/// Recursive φ-subdivision producing a Fibonacci-style spiral of rects. -/// `cw = true` spirals inward clockwise, `false` counter-clockwise. -fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { - let mut out = Vec::new(); - let mut rect = initial; - const PHI_COMPLEMENT: f32 = 0.381_966; // 1 - 1/φ - - for step in 0..max_depth { - if rect.w < 6 || rect.h < 4 { - break; - } - let vertical_split = rect.w >= rect.h; - // Alternate which side the leaf sits on each step; `cw` flips polarity. - let leaf_far = ((step % 2 == 0) ^ !cw) != false; - - if vertical_split { - let leaf_w = ((rect.w as f32) * PHI_COMPLEMENT).round().max(3.0) as i32; - if leaf_far { - out.push(Rect { - x: rect.x + rect.w - leaf_w, - y: rect.y, - w: leaf_w, - h: rect.h, - }); - rect.w -= leaf_w; - } else { - out.push(Rect { - x: rect.x, - y: rect.y, - w: leaf_w, - h: rect.h, - }); - rect.x += leaf_w; - rect.w -= leaf_w; - } - } else { - let leaf_h = ((rect.h as f32) * PHI_COMPLEMENT).round().max(2.0) as i32; - if leaf_far { - out.push(Rect { - x: rect.x, - y: rect.y + rect.h - leaf_h, - w: rect.w, - h: leaf_h, - }); - rect.h -= leaf_h; - } else { - out.push(Rect { - x: rect.x, - y: rect.y, - w: rect.w, - h: leaf_h, - }); - rect.y += leaf_h; - rect.h -= leaf_h; - } - } - } - out.push(rect); - out -} - -/// Golden-ratio child rect inside `parent` — size = parent × 1/φ, random offset -/// snapped to one of the golden-ratio anchor points. -fn golden_child(parent: Rect) -> Rect { - const INV_PHI: f32 = 0.618_034; - let cw = ((parent.w as f32) * INV_PHI).round().max(4.0) as i32; - let ch = ((parent.h as f32) * INV_PHI).round().max(3.0) as i32; - let cw = cw.min(parent.w - 1); - let ch = ch.min(parent.h - 1); - // Pick a corner bias — 4 golden anchors (φ/1-φ combinations). - let bias_x = if r_f32() < 0.5 { 0.0 } else { 1.0 - INV_PHI }; - let bias_y = if r_f32() < 0.5 { 0.0 } else { 1.0 - INV_PHI }; - let x = parent.x + ((parent.w - cw) as f32 * bias_x).round() as i32; - let y = parent.y + ((parent.h - ch) as f32 * bias_y).round() as i32; - Rect { x, y, w: cw, h: ch } -} - -// ───────────────────────── types ───────────────────────── -#[derive(Clone, Copy, PartialEq, Eq)] -enum Side { - L, - R, - Chaos, - Nested, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum FlowDir { - RowMajor, - RowMajorRev, - ColMajor, - ColMajorRev, -} - -fn random_flow() -> FlowDir { - match r_u32() & 3 { - 0 => FlowDir::RowMajor, - 1 => FlowDir::RowMajorRev, - 2 => FlowDir::ColMajor, - _ => FlowDir::ColMajorRev, - } -} - -#[inline] -fn cell_index(rx: i32, ry: i32, w: i32, h: i32, dir: FlowDir) -> i64 { - let rx = rx as i64; - let ry = ry as i64; - let w = w as i64; - let h = h as i64; - match dir { - FlowDir::RowMajor => ry * w + rx, - FlowDir::RowMajorRev => (h - 1 - ry) * w + (w - 1 - rx), - FlowDir::ColMajor => rx * h + ry, - FlowDir::ColMajorRev => (w - 1 - rx) * h + (h - 1 - ry), - } -} - -/// Bespoke algorithms. Each zone becomes a compressed/algorithmic/text signal -/// expression — no smooth waves or lissajous curves, just chunky block/text art. -#[derive(Clone, Copy)] -enum Formation { - /// Spinning wireframe sphere raytraced into the zone as an intensity ramp. - Raytrace, - /// Hard-stepped {░▒▓█} strata scrolling vertically. - BlockStrata, - /// Hex memory dump: `xxxx: AB CD EF ...` - ParseDump, - /// Named registers with values: `R0: 0x4FE8D1A2` - RegisterDump, - /// Stream char → arrow → ROT13 / nibble transform. - TextmarkConverter, - /// Elementary CA (rule 30 or 110) seeded from stream bits. - Cellular1D { rule: u8 }, - /// Scrolling stream text with bright middle band, dim above/below. - Marquee, - /// 2-cell-block density mosaic from stream bytes. - DensityGrid, - /// Sparse transformer-attention pattern — diagonal band + sink cols + hotspots. - AttentionMatrix, - /// Probability distribution over candidate next tokens — top-K bars, - /// sorted by mass, values derived from stream. This is literally what a - /// language model *is* at any moment — a distribution. - ProbField, - /// Converted image (tiger/viper/etc) rendered via full dotmax pipeline - /// (ImageRenderer → BrailleGrid). Subject to glitching effects. - ImagePanel { asset: usize }, - /// Wireframe cube on BLACK background — kept for future use. - #[allow(dead_code)] - RaytraceCube, - /// ATM panel — top-corner balance display with bordered frame. - Atm, - /// "ENTER AGENT" slot — labeled panel for one player. - AgentSlot { player: u8 }, - /// Live chess board rendered via dotmax::chess from a shakmaty position. - ChessBoard, - /// Big "[ CASH OUT ]" payout button. - PayoutButton, - /// Live text input — captures typing, shows prompt + buffer + cursor. - TerminalInput, -} - -/// Important UI text (labels, balance digits, button text) is bright WHITE -/// so it stands out from the deep-red field. Use this for any UI chrome -/// that absolutely must read clearly. -const UI_WHITE: (u8, u8, u8) = (255, 255, 255); - -/// A single dither-variant render of an image. -struct ImageVariant { - cells: Vec>, - w: usize, - h: usize, -} - -/// An image with multiple dither variants pre-rendered. Paint time picks -/// a variant per-cell via a wave function so different dither styles -/// sweep across the image over time. -struct ImageAsset { - name: &'static str, - variants: Vec, // one per dither method - luma: Vec, // raw pattern bytes from first variant — pipe payload -} - -const DITHER_METHODS: &[DitheringMethod] = &[ - DitheringMethod::None, - DitheringMethod::FloydSteinberg, - DitheringMethod::Bayer, - DitheringMethod::Atkinson, -]; - -/// Load and convert one image, rendering every dither method in -/// DITHER_METHODS as separate variants. -fn load_image( - path: &str, - name: &'static str, - cells_w: usize, - cells_h: usize, -) -> Option { - let mut variants: Vec = Vec::with_capacity(DITHER_METHODS.len()); - let mut luma: Option> = None; - for &m in DITHER_METHODS { - let grid = ImageRenderer::new() - .load_from_path(Path::new(path)) - .ok()? - .resize(cells_w, cells_h, true) - .ok()? - .dithering(m) - .render() - .ok()?; - let (gw, gh) = grid.dimensions(); - let mut cells: Vec> = vec![vec![' '; gw]; gh]; - for y in 0..gh { - for x in 0..gw { - cells[y][x] = grid.get_char(x, y); - } - } - if luma.is_none() { - luma = Some(grid.get_raw_patterns().to_vec()); - } - variants.push(ImageVariant { - cells, - w: gw, - h: gh, - }); - } - Some(ImageAsset { - name, - variants, - luma: luma.unwrap_or_default(), - }) -} - -fn load_image_assets() -> Vec { - // Heavy on tigers, snakes, rabbits. A little frog. Some grifter. - let candidates: &[(&str, &str, &'static str)] = &[ - ( - "tests/fixtures/images/tiger_small.png", - "./tests/fixtures/images/tiger_small.png", - "TIGER", - ), - ( - "tests/fixtures/images/tiger_1.png", - "./tests/fixtures/images/tiger_1.png", - "TIGR2", - ), - ( - "tests/fixtures/images/viper3.png", - "./tests/fixtures/images/viper3.png", - "VIPER", - ), - ( - "tests/fixtures/images/viper_head_3.png", - "./tests/fixtures/images/viper_head_3.png", - "VHEAD", - ), - ( - "tests/fixtures/images/extras/snakedesk.png", - "./tests/fixtures/images/extras/snakedesk.png", - "SNAKE", - ), - ( - "tests/fixtures/images/extras/rabbit.png", - "./tests/fixtures/images/extras/rabbit.png", - "RABT", - ), - ( - "tests/fixtures/images/extras/grifter.jpg", - "./tests/fixtures/images/extras/grifter.jpg", - "GRFTR", - ), - ( - "tests/fixtures/images/extras/frog_01.png", - "./tests/fixtures/images/extras/frog_01.png", - "FROG", - ), - ( - "tests/fixtures/images/extras/frog_02.png", - "./tests/fixtures/images/extras/frog_02.png", - "FROG2", - ), - ]; - let mut out = Vec::new(); - for &(p1, p2, name) in candidates { - if let Some(a) = load_image(p1, name, 64, 32).or_else(|| load_image(p2, name, 64, 32)) { - out.push(a); - } - } - out -} - -fn pick_formation(rect: Rect) -> Formation { - let aspect = (rect.w as f32) / (rect.h.max(1) as f32); - let r = r_u32() as usize; - if aspect > 3.5 { - // Very wide — horizontal readouts. - match r % 4 { - 0 => Formation::Marquee, - 1 => Formation::TextmarkConverter, - 2 => Formation::BlockStrata, - _ => Formation::RegisterDump, - } - } else if aspect < 0.65 { - // Tall/narrow — vertical-friendly stuff. - match r % 3 { - 0 => Formation::ParseDump, - 1 => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - _ => Formation::BlockStrata, - } - } else if (aspect - 1.0).abs() < 0.45 && rect.w >= 10 && rect.h >= 6 { - // Square-ish + large enough — save the wow formations for here. - match r % 4 { - 0 => Formation::Raytrace, - 1 => Formation::AttentionMatrix, - 2 => Formation::DensityGrid, - _ => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - } - } else { - // Mid aspect — everything fair game. - match r % 9 { - 0 => Formation::ParseDump, - 1 => Formation::RegisterDump, - 2 => Formation::DensityGrid, - 3 => Formation::TextmarkConverter, - 4 => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - 5 => Formation::BlockStrata, - 6 => Formation::AttentionMatrix, - 7 => Formation::ProbField, - _ => Formation::Marquee, - } - } -} - -struct Zone { - base_rect: Rect, // locked — this is also what's rendered - rect: Rect, // kept for convenience; equals base_rect always - - side: Side, - formation: Formation, - flow_dir: FlowDir, - tap_offset: i32, - pulse: f32, - pulse_rate: f32, - glitch_rate: f32, -} - -fn make_zone( - base: Rect, - side: Side, - formation: Formation, - flow_dir: FlowDir, - tap: i32, - _zone_idx: usize, -) -> Zone { - Zone { - base_rect: base, - rect: base, - side, - formation, - flow_dir, - tap_offset: tap, - pulse: r_f32() * 3.0, - pulse_rate: 0.6 + r_f32() * 1.1, - glitch_rate: if r_f32() < 0.15 { - 0.3 + r_f32() * 0.7 - } else { - 0.0 - }, - } -} - -struct Scene { - w: i32, - h: i32, - stream: Vec, - cursor: f32, - flow_rate: f32, - zones: Vec, - pipes: Vec, - assets: Vec, - adjacency: Vec>, - /// Persistent overlay flow lines (orthogonal + crossed diagonals). - streamers: Vec, - /// Edge-anchored noise injection feeds. - noise_feeds: Vec, - /// Rects that streamers/feeds skip — cube window + UI rects (ATM, - /// AgentSlots, ChessBoard, PayoutButton, TerminalInput). - protected_rects: Vec, - /// Live chess game played by random legal moves — the betting subject. - chess_pos: Chess, - /// Cursor value when last chess move was played. - chess_last_move_at: f32, - /// ATM balance — visible on the panel. - balance: u32, - /// Cursor when balance last ticked (jitters every ~0.3s with ±2% swings). - balance_last_tick: f32, - /// Live text input from the keyboard. - input_buffer: String, - /// Active short-lived image fragments blasted on top of the scene. - glitch_inserts: Vec, - /// Cursor when the last glitch insert was spawned. - last_glitch_spawn: f32, - /// Wandering dither worms — block-density trails that crawl through grids. - dither_flows: Vec, - flipped: bool, - reversed: bool, -} - -/// Bundle of everything a paint function might need to read. -struct PaintCtx<'a> { - stream: &'a [char], - cursor: f32, - zones: &'a [Zone], - adjacency: &'a [Vec], - assets: &'a [ImageAsset], - chess_pos: &'a Chess, - balance: u32, - input_buffer: &'a str, -} - -// ───────────────────────── scene construction ───────────────────────── -fn build_scene(w: i32, h: i32) -> Scene { - seed_from_clock(); - - let mid = w / 2; - // Depth scales with size — no slivers on tiny terminals. - let depth = ((w.min(h * 2)) / 14).clamp(4, 8) as usize; - - let left_spiral = fib_spiral( - Rect { - x: 0, - y: 0, - w: mid, - h, - }, - depth, - true, - ); - let right_spiral = fib_spiral( - Rect { - x: mid, - y: 0, - w: w - mid, - h, - }, - depth, - false, - ); - - let mut zones = Vec::new(); - let mut tap_accum: i64 = 0; - - // LEFT: walk outermost → innermost. Default flow RowMajor. - for rect in &left_spiral { - let side = if r_f32() < 0.08 { Side::Chaos } else { Side::L }; - let flow_dir = if r_f32() < 0.20 { - random_flow() - } else { - FlowDir::RowMajor - }; - let formation = pick_formation(*rect); - let idx = zones.len(); - zones.push(make_zone( - *rect, - side, - formation, - flow_dir, - tap_accum as i32, - idx, - )); - tap_accum += (rect.w * rect.h) as i64; - } - - // RIGHT: walk innermost → outermost so the hose reverses direction, - // creating the mirrored flow. Default flow RowMajorRev. - for rect in right_spiral.iter().rev() { - let side = if r_f32() < 0.08 { Side::Chaos } else { Side::R }; - let flow_dir = if r_f32() < 0.20 { - random_flow() - } else { - FlowDir::RowMajorRev - }; - let formation = pick_formation(*rect); - let idx = zones.len(); - zones.push(make_zone( - *rect, - side, - formation, - flow_dir, - tap_accum as i32, - idx, - )); - tap_accum += (rect.w * rect.h) as i64; - } - - // NESTED CHILDREN — overlay on the 4 biggest zones so the composition has - // "boxes within boxes" at golden-ratio insets. - let mut big_indices: Vec = (0..zones.len()).collect(); - big_indices.sort_by_key(|&i| -(zones[i].base_rect.w * zones[i].base_rect.h)); - for &i in big_indices.iter().take(4) { - let parent = zones[i].base_rect; - if parent.w < 12 || parent.h < 6 { - continue; - } - let child = golden_child(parent); - if child.w < 5 || child.h < 3 { - continue; - } - let formation = pick_formation(child); - let idx = zones.len(); - let mut z = make_zone( - child, - Side::Nested, - formation, - random_flow(), - tap_accum as i32, - idx, - ); - z.glitch_rate = 0.15 + r_f32() * 0.4; - zones.push(z); - tap_accum += (child.w * child.h) as i64; - } - - // Load image assets now so we can assign specific zones to display them. - let assets = load_image_assets(); - - // ─── Size-aware layout ─── compute every UI rect from (w, h) so the - // betting interface scales up to fill any terminal size, always large. - // - // Chess is centered. Square aspect: cell width = 2 × cell height (since - // braille cells are ~2:1 tall). - let chess_h = ((h as f32 * 0.55) as i32).clamp(8, 36); - let mut chess_w = chess_h * 2; - if chess_w > w * 5 / 8 { - chess_w = (w * 5 / 8) & !1; // even - // recompute height to maintain aspect - } - let chess_w = chess_w.clamp(16, 80); - let chess_h = (chess_w / 2).clamp(8, 36); - let ui_chess = Rect { - x: (w - chess_w) / 2, - y: ((h - chess_h) / 2 - 1).max(2), - w: chess_w, - h: chess_h, - }; - - let panel_w = (w / 7).clamp(18, 28); - let panel_h = (h / 9).clamp(4, 6); - - let ui_atm = Rect { - x: w - panel_w - 1, - y: 1, - w: panel_w, - h: panel_h, - }; - let ui_agent_a = Rect { - x: 1, - y: 1, - w: panel_w, - h: panel_h, - }; - let ui_agent_b = Rect { - x: w - panel_w - 1, - y: ui_atm.y + ui_atm.h + 1, - w: panel_w, - h: panel_h, - }; - let ui_payout = Rect { - x: w - panel_w - 1, - y: ui_agent_b.y + ui_agent_b.h + 1, - w: panel_w, - h: panel_h.min(4), - }; - - let term_h = 3_i32; - let term_w = (chess_w + 4).min(w - 4); - let ui_term = Rect { - x: (w - term_w) / 2, - y: h - term_h - 1, - w: term_w, - h: term_h, - }; - - // ─── Dedicated SNAKE image slots ─── carved next to the chess board so - // the vipers stay visible and big. Tall narrow strips on each side. - let img_left_h = (ui_term.y - (ui_agent_a.y + ui_agent_a.h) - 2).max(8); - let ui_viper = Rect { - x: 1, - y: ui_agent_a.y + ui_agent_a.h + 1, - w: panel_w, - h: img_left_h, - }; - let img_right_h = (ui_term.y - (ui_payout.y + ui_payout.h) - 2).max(6); - let ui_vhead = Rect { - x: w - panel_w - 1, - y: ui_payout.y + ui_payout.h + 1, - w: panel_w, - h: img_right_h, - }; - - // Sort the surviving zones by area for asset/formation assignment. - let mut sorted_by_area: Vec = (0..zones.len()) - .filter(|&i| zones[i].side != Side::Nested) - .collect(); - sorted_by_area.sort_by_key(|&i| -(zones[i].base_rect.w * zones[i].base_rect.h)); - - // Seed image panels into the biggest non-Nested survivors. - if !assets.is_empty() { - let mut assigned = 0usize; - let want = assets.len().min(sorted_by_area.len()); - for &i in &sorted_by_area { - let r = zones[i].base_rect; - if r.w < 10 || r.h < 6 { - continue; - } - zones[i].formation = Formation::ImagePanel { - asset: assigned % assets.len(), - }; - assigned += 1; - if assigned >= want { - break; - } - } - } - - let _ui_rects = [ - ui_atm, ui_agent_a, ui_agent_b, ui_chess, ui_payout, ui_term, ui_viper, ui_vhead, - ]; - // NOTE: fib zones are NOT filtered — chaos paints under everything, - // and UI re-paints on top of the chaos in a final pass (see render()). - - // Push UI zones. Each is Side::Nested so they don't participate in pipes. - let mut push_ui = |rect: Rect, formation: Formation, tap: &mut i64| { - zones.push(Zone { - base_rect: rect, - rect, - side: Side::Nested, - formation, - flow_dir: FlowDir::RowMajor, - tap_offset: *tap as i32, - pulse: r_f32() * 3.0, - pulse_rate: 0.8 + r_f32() * 0.5, - glitch_rate: 0.0, - }); - *tap += (rect.w * rect.h) as i64; - }; - push_ui(ui_atm, Formation::Atm, &mut tap_accum); - push_ui( - ui_agent_a, - Formation::AgentSlot { player: 0 }, - &mut tap_accum, - ); - push_ui( - ui_agent_b, - Formation::AgentSlot { player: 1 }, - &mut tap_accum, - ); - push_ui(ui_chess, Formation::ChessBoard, &mut tap_accum); - push_ui(ui_payout, Formation::PayoutButton, &mut tap_accum); - push_ui(ui_term, Formation::TerminalInput, &mut tap_accum); - // SCARY SNAKES — guaranteed visible at decent size. - let viper_idx = if assets.len() > 1 { 1 } else { 0 }; - let vhead_idx = if assets.len() > 2 { 2 } else { viper_idx }; - push_ui( - ui_viper, - Formation::ImagePanel { asset: viper_idx }, - &mut tap_accum, - ); - push_ui( - ui_vhead, - Formation::ImagePanel { asset: vhead_idx }, - &mut tap_accum, - ); - - let stream = build_stream(32_768); - let pipes: Vec = build_pipes(&zones); - - // Adjacency: neighbors = zones a pipe actually connects. - let mut adjacency: Vec> = vec![Vec::new(); zones.len()]; - for p in &pipes { - adjacency[p.from as usize].push(p.to); - adjacency[p.to as usize].push(p.from); - } - - // Persistent overlay streamers — many axes for synapse density. - let streamers = vec![ - // Horizontals at varied rows - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.06) as i32, - speed: 26.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.42) as i32, - speed: 32.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.78) as i32, - speed: 21.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.94) as i32, - speed: 18.0, - direction: -1, - }, - // Verticals on far edges - Streamer { - axis: StreamerAxis::Vertical, - anchor: (w as f32 * 0.04) as i32, - speed: 22.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::Vertical, - anchor: (w as f32 * 0.97) as i32, - speed: 28.0, - direction: 1, - }, - // Diagonals at multiple intercepts - Streamer { - axis: StreamerAxis::DiagPos, - anchor: (w as f32 * 0.02) as i32, - speed: 18.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::DiagPos, - anchor: (w as f32 * 0.45) as i32, - speed: 24.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::DiagNeg, - anchor: (w as f32 * 0.98) as i32, - speed: 17.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::DiagNeg, - anchor: (w as f32 * 0.55) as i32, - speed: 23.0, - direction: -1, - }, - ]; - - // Noise projection feeds — all 4 edges, dense. - let noise_feeds = vec![ - // Top edge - NoiseFeed { - pos: ((w as f32 * 0.10) as i32, 0), - dir: (0, 1), - length: 4, - seed: 0xACE0_BEEF, - speed: 11.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.30) as i32, 0), - dir: (0, 1), - length: 5, - seed: 0xFACE_FADE, - speed: 13.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.50) as i32, 0), - dir: (0, 1), - length: 3, - seed: 0xB001_C0DE, - speed: 15.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.70) as i32, 0), - dir: (0, 1), - length: 4, - seed: 0x1337_C0DE, - speed: 12.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.90) as i32, 0), - dir: (0, 1), - length: 3, - seed: 0xBEEF_F00D, - speed: 16.0, - }, - // Left edge - NoiseFeed { - pos: (0, (h as f32 * 0.30) as i32), - dir: (1, 0), - length: 5, - seed: 0xDEAD_BEEF, - speed: 12.0, - }, - NoiseFeed { - pos: (0, (h as f32 * 0.55) as i32), - dir: (1, 0), - length: 4, - seed: 0xCAFE_F00D, - speed: 14.0, - }, - NoiseFeed { - pos: (0, (h as f32 * 0.78) as i32), - dir: (1, 0), - length: 5, - seed: 0xFEED_BABE, - speed: 10.0, - }, - // Right edge - NoiseFeed { - pos: (w - 1, (h as f32 * 0.30) as i32), - dir: (-1, 0), - length: 5, - seed: 0xDEAD_C0DE, - speed: 14.0, - }, - NoiseFeed { - pos: (w - 1, (h as f32 * 0.55) as i32), - dir: (-1, 0), - length: 4, - seed: 0x4269_4269, - speed: 11.0, - }, - NoiseFeed { - pos: (w - 1, (h as f32 * 0.78) as i32), - dir: (-1, 0), - length: 5, - seed: 0xC001_BEEF, - speed: 15.0, - }, - // Bottom edge - NoiseFeed { - pos: ((w as f32 * 0.20) as i32, h - 1), - dir: (0, -1), - length: 4, - seed: 0xC0DE_F00D, - speed: 12.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.55) as i32, h - 1), - dir: (0, -1), - length: 4, - seed: 0xBEEF_BABE, - speed: 13.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.85) as i32, h - 1), - dir: (0, -1), - length: 5, - seed: 0xFA15_AFE1, - speed: 11.0, - }, - ]; - - // No protected rects — chaos bleeds everywhere. UI re-paints on top. - let protected_rects: Vec = Vec::new(); - - Scene { - w, - h, - stream, - cursor: 0.0, - flow_rate: 48.0, - zones, - pipes, - assets, - adjacency, - streamers, - noise_feeds, - protected_rects, - chess_pos: Chess::default(), - chess_last_move_at: 0.0, - balance: 42_069, - balance_last_tick: 0.0, - input_buffer: String::new(), - glitch_inserts: Vec::new(), - last_glitch_spawn: 0.0, - dither_flows: { - let mut flows = Vec::new(); - for _ in 0..6 { - let speed = 6.0 + r_f32() * 12.0; - let theta = r_f32() * std::f32::consts::TAU; - flows.push(DitherFlow { - pos_x: r_f32() * w as f32, - pos_y: r_f32() * h as f32, - vel_x: theta.cos() * speed, - vel_y: theta.sin() * speed * 0.5, // y velocity halved (terminal aspect) - trail: Vec::new(), - trail_max: 14 + (r_u32() as usize % 18), - }); - } - flows - }, - flipped: false, - reversed: false, - } -} - -// ───────────────────────── simulation ───────────────────────── -fn tick(scene: &mut Scene, dt: f32) { - let sign = if scene.reversed { -1.0 } else { 1.0 }; - scene.cursor += scene.flow_rate * dt * sign; - for z in &mut scene.zones { - z.pulse += dt * z.pulse_rate * sign; - } - // Advance the chess game — one random legal move every ~1.2 seconds (60 cursor units). - if scene.cursor - scene.chess_last_move_at > 60.0 { - scene.chess_last_move_at = scene.cursor; - let moves = scene.chess_pos.legal_moves(); - if moves.is_empty() { - scene.chess_pos = Chess::default(); - } else { - let idx = (r_u32() as usize) % moves.len(); - let mv = moves[idx]; - scene.chess_pos.play_unchecked(mv); - } - } - // Balance tick — every ~14 cursor units (~0.3s) jitter by ±~2%. - if scene.cursor - scene.balance_last_tick > 14.0 { - scene.balance_last_tick = scene.cursor; - let pct = (r_f32() - 0.5) * 0.04; // ±2% - let delta = (scene.balance as f32 * pct) as i64; - let new_bal = (scene.balance as i64 + delta).max(100); - scene.balance = new_bal as u32; - } - - // Tick wandering dither flows. - let w = scene.w; - let h = scene.h; - for flow in &mut scene.dither_flows { - tick_flow(flow, dt * sign, w, h); - } - - // Glitch insertions — random image chunks blasted onto the screen. - // Expire dead ones first. - let cur = scene.cursor; - scene - .glitch_inserts - .retain(|i| (cur - i.spawn_cursor).abs() < i.duration_chars); - // Then maybe spawn a new one. Up to 5 active simultaneously. - if scene.cursor - scene.last_glitch_spawn > 25.0 - && scene.glitch_inserts.len() < 5 - && !scene.assets.is_empty() - { - scene.last_glitch_spawn = scene.cursor; - if r_f32() < 0.85 { - spawn_glitch_insertion(scene); - } - } -} - -fn spawn_glitch_insertion(scene: &mut Scene) { - let asset_idx = (r_u32() as usize) % scene.assets.len(); - let asset = &scene.assets[asset_idx]; - if asset.variants.is_empty() { - return; - } - let variant_idx = (r_u32() as usize) % asset.variants.len(); - let variant = &asset.variants[variant_idx]; - let aw = variant.w as i32; - let ah = variant.h as i32; - - // Random size + position. Chunks range from small to half-screen. - let max_w = (scene.w / 2).max(8); - let max_h = (scene.h * 2 / 3).max(6); - let rw = (8 + (r_u32() as i32 % (max_w - 7).max(1))).min(scene.w - 1); - let rh = (4 + (r_u32() as i32 % (max_h - 3).max(1))).min(scene.h - 1); - let rx = r_u32() as i32 % (scene.w - rw).max(1); - let ry = r_u32() as i32 % (scene.h - rh).max(1); - let rect = Rect { - x: rx, - y: ry, - w: rw, - h: rh, - }; - - // Half the time: full image. Other half: random crop (a strip or chunk). - let crop = if r_f32() < 0.5 { - None - } else { - let cw = (4 + (r_u32() as i32 % (aw - 3).max(1))).min(aw); - let ch = (3 + (r_u32() as i32 % (ah - 2).max(1))).min(ah); - let cx = r_u32() as i32 % (aw - cw).max(1); - let cy = r_u32() as i32 % (ah - ch).max(1); - Some(Rect { - x: cx, - y: cy, - w: cw, - h: ch, - }) - }; - - let duration_chars = 30.0 + r_f32() * 90.0; // ~0.6 to ~2.5 sec @ 48 cps - scene.glitch_inserts.push(GlitchInsertion { - asset_idx, - rect, - crop, - spawn_cursor: scene.cursor, - duration_chars, - variant_idx, - }); -} - -// ───────────────────────── rendering ───────────────────────── -/// Sentinel value meaning "no zone owns this cell yet." -const NO_OWNER: u16 = u16::MAX; - -#[derive(Clone, Copy)] -struct PxCell { - ch: char, - fg: (u8, u8, u8), - intensity: f32, - owner: u16, // zone index that won this cell — used for hard-cutoff masks -} -impl PxCell { - const fn empty() -> Self { - Self { - ch: ' ', - fg: (0, 0, 0), - intensity: 0.0, - owner: NO_OWNER, - } - } -} - -fn put(grid: &mut [Vec], x: i32, y: i32, ch: char, c: (u8, u8, u8), i: f32, owner: u16) { - if y < 0 || x < 0 { - return; - } - let (uy, ux) = (y as usize, x as usize); - if uy >= grid.len() || ux >= grid[0].len() { - return; - } - let cell = &mut grid[uy][ux]; - if i >= cell.intensity { - cell.ch = ch; - cell.fg = c; - cell.intensity = i; - cell.owner = owner; - } -} - -/// Forced paint — used by pipes to bleed across zone boundaries regardless -/// of who owns the cell. Always overwrites. -fn put_force(grid: &mut [Vec], x: i32, y: i32, ch: char, c: (u8, u8, u8), i: f32) { - if y < 0 || x < 0 { - return; - } - let (uy, ux) = (y as usize, x as usize); - if uy >= grid.len() || ux >= grid[0].len() { - return; - } - grid[uy][ux] = PxCell { - ch, - fg: c, - intensity: i, - owner: NO_OWNER, - }; -} - -fn color_for(side: Side) -> (u8, u8, u8) { - // Pure grayscale — signal comes from intensity + char-weight, not hue. - // Side identity survives as small brightness differences at full intensity. - match side { - Side::L | Side::R => (170, 18, 18), // deep matte blood red - Side::Chaos => (255, 90, 90), // hot pink-red pops through - Side::Nested => (230, 40, 40), // bright red, not quite hot - } -} - -#[inline] -fn sample(stream: &[char], idx: i64) -> char { - let n = stream.len() as i64; - stream[idx.rem_euclid(n) as usize] -} - -/// Occasional discrete phase jumps, modulated by zone.pulse. -fn glitch_offset(z: &Zone) -> i64 { - if z.glitch_rate < 0.05 { - return 0; - } - let phase = (z.pulse * z.glitch_rate * 0.6) as i64; - // wrapping_mul by a prime gives chaotic jumps when phase increments. - phase.wrapping_mul(2_039) -} - -// ─────────────── formation paint helpers ─────────────── - -fn ihash(x: i32, y: i32, t: i32) -> u32 { - let mut n = (x as u32) - .wrapping_mul(374_761_393) - .wrapping_add((y as u32).wrapping_mul(668_265_263)) - .wrapping_add((t as u32).wrapping_mul(2_654_435_761)); - n ^= n >> 13; - n = n.wrapping_mul(1_274_126_177); - n ^ (n >> 16) -} - -fn paint_fill(grid: &mut [Vec], zone: &Zone, zone_id: u16, ch: char, i: f32) { - let color = color_for(zone.side); - for ry in 0..zone.rect.h { - for rx in 0..zone.rect.w { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 1. Raytrace window — spinning wireframe sphere. The wow. -fn paint_raytrace(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let w = zone.rect.w as usize; - let h = zone.rect.h as usize; - if w < 4 || h < 3 { - paint_fill(grid, zone, zone_id, '·', 0.20); - return; - } - let mut rt = RtScene::new(); - rt.add_object(Box::new(Sphere::new(Vector3::new(0.0, 0.0, -3.0), 1.1))); - let cam = Camera::new(Vector3::new(0.0, 0.0, 0.0), 4.0, 3.0); - let orient = WireframeRotation { - yaw: zone.pulse * 0.6, - pitch: (zone.pulse * 0.4).sin() * 0.45, - roll: 0.0, - }; - let mode = RenderMode::Wireframe { - step_rad: 15.0_f32.to_radians(), - tol_rad: 0.035, - }; - let buf = render_with_orientation(&rt, &cam, w, h, mode, orient); - - let ramp: &[char] = &[' ', '·', ':', '-', '=', '+', '*', '#', '%', '@']; - for ry in 0..h { - for rx in 0..w { - let v = buf[ry][rx].clamp(0.0, 1.0); - let idx = ((v * (ramp.len() - 1) as f32).round() as usize).min(ramp.len() - 1); - let ch = ramp[idx]; - let i = if v > 0.30 { 0.90 } else { 0.22 }; - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry as i32, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 2. BlockStrata — hard-stepped density bands, no smooth interp. -fn paint_block_strata(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let levels: &[(char, f32)] = &[ - (' ', 0.08), - ('░', 0.38), - ('▒', 0.62), - ('▓', 0.85), - ('█', 1.00), - ]; - let scroll = (zone.pulse * 2.4) as i32; - // Step-function over y: 20-row cycle with custom profile. - for ry in 0..zone.rect.h { - let stripe = (ry + scroll).rem_euclid(20); - let level_idx = match stripe { - 0..=1 => 0, - 2..=4 => 1, - 5..=8 => 2, - 9..=12 => 3, - 13..=15 => 4, - 16..=18 => 3, - _ => 2, - }; - let (ch, i) = levels[level_idx]; - for rx in 0..zone.rect.w { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 3. ParseDump — `xxxx: AB CD EF ...` hex memory dump. -fn paint_parse_dump( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let scroll = base / 4; - - for ry in 0..zh { - let addr = (scroll.wrapping_add(ry as i64) & 0xffff) as u32; - for rx in 0..zw { - let (ch, i) = if rx < 4 { - let nibble = ((addr >> ((3 - rx) * 4)) & 0xf) as usize; - (HEX[nibble], 0.72) - } else if rx == 4 { - (':', 0.55) - } else if rx == 5 { - (' ', 0.10) - } else { - let rel = rx - 6; - let byte_idx = rel / 3; - let pos = rel % 3; - let b = sample(stream, base + (ry as i64) * 9 + byte_idx as i64) as u32; - match pos { - 0 => (HEX[((b >> 4) & 0xf) as usize], 0.92), - 1 => (HEX[(b & 0xf) as usize], 0.92), - _ => (' ', 0.12), - } - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 4. RegisterDump — named registers with hex values. -fn paint_register_dump( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - const NAMES: &[&str] = &[ - "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7", "PC", "SP", "LR", "SR", - ]; - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - - for ry in 0..zh { - let name_cycle = ((base / 8) as usize).wrapping_add(ry as usize) % NAMES.len(); - let name = NAMES[name_cycle]; - let mut row: Vec<(char, f32)> = Vec::with_capacity(zw as usize); - for ch in name.chars() { - row.push((ch, 0.82)); - } - row.push((':', 0.55)); - row.push((' ', 0.10)); - row.push(('0', 0.70)); - row.push(('x', 0.70)); - for i in 0..8 { - let nib = sample(stream, base + (ry as i64) * 5 + i as i64) as u32; - row.push((HEX[(nib & 0xf) as usize], 0.95)); - } - while row.len() < zw as usize { - row.push((' ', 0.10)); - } - for (rx, &(ch, i)) in row.iter().take(zw as usize).enumerate() { - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 5. TextmarkConverter — left side raw stream → `⇒` → right side transformed. -fn paint_textmark( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let mid_y = zh / 2; - let mid_x = zw / 2; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - - let transform = |c: char| -> char { - if c.is_ascii_alphabetic() { - let b = if c.is_ascii_lowercase() { b'a' } else { b'A' }; - let off = ((c as u8) - b + 13) % 26; - (b + off) as char - } else if c.is_ascii_digit() { - let d = (c as u8) - b'0'; - (b'0' + (9 - d)) as char - } else if c.is_ascii() { - HEX[((c as u8) >> 4 & 0x0f) as usize] - } else { - HEX[((c as u32) & 0x0f) as usize] - } - }; - - for ry in 0..zh { - for rx in 0..zw { - if ry == mid_y && rx == mid_x { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - '⇒', - color, - 1.0, - zone_id, - ); - continue; - } - if ry == mid_y { - let (ch, i) = if rx < mid_x { - let c = sample(stream, base + (mid_x - 1 - rx) as i64); - (c, 0.95) - } else { - let c = sample(stream, base + (rx - mid_x - 1) as i64); - (transform(c), 0.95) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } else { - let d = ((ry - mid_y).abs() as f32) / (zh as f32 * 0.5); - let fade = (0.48 - d * 0.32).max(0.12); - let c = sample(stream, base + (ry as i64) * 7 + rx as i64); - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - c, - color, - fade, - zone_id, - ); - } - } - } -} - -/// 6. Cellular1D — elementary CA, seeded from the stream, evolves top-to-bottom. -fn paint_cellular( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - rule: u8, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w as usize; - let zh = zone.rect.h as usize; - if zw < 3 || zh < 2 { - paint_fill(grid, zone, zone_id, '·', 0.20); - return; - } - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let t_shift = (zone.pulse * 2.0) as i64; - - // Seed top row from stream bits. - let mut row = vec![false; zw]; - for rx in 0..zw { - let s = sample(stream, base + rx as i64 + t_shift) as u32; - row[rx] = (s & 1) == 1; - } - // Paint row, then evolve. - for ry in 0..zh { - for rx in 0..zw { - let (ch, i) = if row[rx] { ('█', 0.93) } else { ('·', 0.18) }; - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry as i32, - ch, - color, - i, - zone_id, - ); - } - if ry + 1 >= zh { - break; - } - let prev = row.clone(); - for rx in 0..zw { - let l = prev[(rx + zw - 1) % zw]; - let c = prev[rx]; - let r = prev[(rx + 1) % zw]; - let pat = ((l as u8) << 2) | ((c as u8) << 1) | (r as u8); - row[rx] = ((rule >> pat) & 1) == 1; - } - } -} - -/// 8. Marquee — scrolling stream text with bright middle band. -fn paint_marquee( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let base: i64 = (cursor as i64) - (zone.tap_offset as i64) + glitch_offset(zone); - let zw = zone.rect.w; - let zh = zone.rect.h; - let mid_y = zh / 2; - for ry in 0..zh { - for rx in 0..zw { - let ci = cell_index(rx, ry, zw, zh, zone.flow_dir); - let ch = sample(stream, base + ci); - let i = if ry == mid_y { - 1.0 - } else { - let d = ((ry - mid_y).abs() as f32) / (zh as f32 * 0.5); - (0.78 - d * 0.48).max(0.30) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 9. DensityGrid — 2-cell block mosaic at stream-byte density. -fn paint_density_grid( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let levels: &[(char, f32)] = &[ - (' ', 0.08), - ('░', 0.35), - ('▒', 0.58), - ('▓', 0.82), - ('█', 1.00), - ]; - let block_w: i32 = 2; - let blocks_per_row = (zone.rect.w + block_w - 1) / block_w; - for ry in 0..zone.rect.h { - for bx in 0..blocks_per_row { - let rx0 = bx * block_w; - let idx = (ry as i64) * (blocks_per_row as i64) + bx as i64; - let s = sample(stream, base + idx) as u32; - let density = ((s & 0xff) as usize * levels.len()) / 256; - let density = density.min(levels.len() - 1); - let (ch, i) = levels[density]; - for k in 0..block_w { - let rx = rx0 + k; - if rx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } - } -} - -/// 10. AttentionMatrix — sparse transformer-attention pattern: diagonal band, -/// a few sink columns, rare hotspots. Everything else mostly dark. -fn paint_attention(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let t = zone.pulse; - - // A handful of "sink" columns (attention sinks) that migrate slowly. - let n_sinks = (1 + (zw / 14)).max(2); - let mut sinks: Vec = Vec::with_capacity(n_sinks as usize); - for i in 0..n_sinks { - let phase = (t * 0.2 + i as f32 * 0.7).sin(); - let pos = (((phase + 1.0) * 0.5) * (zw as f32 - 2.0)) as i32 + 1; - sinks.push(pos.clamp(0, zw - 1)); - } - - for ry in 0..zh { - for rx in 0..zw { - let mut score: f32 = 0.12; - - // Diagonal band — attending to self/near tokens. - let diag_x = (ry as f32 / zh.max(1) as f32) * (zw as f32); - let d = (diag_x - rx as f32).abs(); - if d < 2.0 { - score = score.max(0.88 - d * 0.25); - } - - // Sink columns — always some attention. - for &s in &sinks { - let cd = (rx - s).abs(); - if cd == 0 { - score = score.max(0.78); - } else if cd == 1 { - score = score.max(0.42); - } - } - - // Rare random hotspots that shimmer with time. - let h = ihash(rx, ry, (t * 2.0) as i32); - if (h & 0xff) < 4 { - score = score.max(0.95); - } - - let (ch, i) = if score > 0.85 { - ('█', 1.0) - } else if score > 0.60 { - ('▓', 0.80) - } else if score > 0.38 { - ('▒', 0.55) - } else if score > 0.18 { - ('░', 0.32) - } else { - ('.', 0.14) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 11. ProbField — top-K token distribution, **conditioned on neighbors**. -/// -/// Each row's probability is derived by sampling the hose at one of this -/// zone's neighbors' current windows. If a zone has no neighbors (isolated, -/// rare), it falls back to its own tap. The ordering by mass is real: the -/// distribution collapses onto a top candidate each frame, with the runners-up -/// visibly competing below it. As the hose advances, the neighbors' views -/// shift, and this zone's entire distribution reshuffles in response — a -/// picture of attention doing what attention does. -fn paint_prob_field(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &PaintCtx) { - const CANDIDATES: &[&str] = &[ - "the", "and", "to", "of", "is", "a", "in", "that", "it", "for", "fn", "::", "0x", "->", - "=>", "if", "fold", "self", "void", "phi", "sigma", "delta", "ROUTINE", "ACK", "MOV", - "yield", "loop", "ok", "recur", "echo", "bind", "map", "tau", "λ", "∇", - ]; - let color = color_for(zone.side); - let zw = zone.rect.w as usize; - let zh = zone.rect.h as usize; - if zw < 14 || zh < 2 { - paint_fill(grid, zone, zone_id, '·', 0.25); - return; - } - - // Collect this zone's neighbors' tap offsets. Fall back to own tap if - // isolated so the formation still reads coherently. - let neighbors = &ctx.adjacency[zone_id as usize]; - let tap_pool: Vec = if neighbors.is_empty() { - vec![zone.tap_offset] - } else { - neighbors - .iter() - .map(|&j| ctx.zones[j as usize].tap_offset) - .collect() - }; - - let top_k = zh.min(16); - let bar_width = zw.saturating_sub(13).max(4); - - // Each row samples from ONE neighbor's current window — the row's weight - // is what that neighbor is "focusing on" right now. Skew-cubed so one or - // two candidates dominate (real LM distributions have heavy peaks). - let mut probs: Vec<(f32, &str)> = Vec::with_capacity(top_k); - let mut sum = 0.0_f32; - for i in 0..top_k { - let tap = tap_pool[i % tap_pool.len()]; - let neighbor_window_offset = (i as i64) * 23 + (ctx.cursor as i64 / 3); - let s = sample( - ctx.stream, - (ctx.cursor as i64) - (tap as i64) + neighbor_window_offset, - ) as u32; - let raw = 0.01 + ((s & 0xff) as f32) / 255.0; - let weight = raw.powi(3); - sum += weight; - let name = CANDIDATES[(s as usize >> 4) % CANDIDATES.len()]; - probs.push((weight, name)); - } - for p in &mut probs { - p.0 /= sum.max(1e-6); - } - probs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); - - paint_fill(grid, zone, zone_id, ' ', 0.08); - - for (row, (p, name)) in probs.iter().enumerate().take(zh) { - let ry = row as i32; - let fill = ((*p * bar_width as f32).round() as usize).min(bar_width); - for rx in 0..(bar_width as i32) { - let (ch, i) = if (rx as usize) < fill { - ('█', (0.55 + p * 0.45).min(1.0)) - } else { - ('░', 0.20) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - let p_str = format!(" {:.2}", p.min(0.99)); - let mut cx = bar_width as i32 + 1; - for ch in p_str.chars() { - if cx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + cx, - zone.rect.y + ry, - ch, - color, - 0.82, - zone_id, - ); - cx += 1; - } - cx += 1; - for ch in name.chars() { - if cx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + cx, - zone.rect.y + ry, - ch, - color, - 0.95, - zone_id, - ); - cx += 1; - } - } -} - -/// 12. ImagePanel — streams a pre-rendered braille image into the zone with -/// glitching effects. Source: dotmax's full ImageRenderer pipeline -/// (Floyd-Steinberg → Otsu → braille mapping). Each frame, a few rows get -/// scanline-torn, a sprinkle of cells get block-char corrupted, and bursts -/// of stream chars bleed through as noise. -// ─────────────── abstract dither-phase system ─────────────── -// -// Instead of a smooth per-cell wave, the composition is in one of two -// macro-phases at any moment: -// -// • Stable — ~11 real-seconds of a SINGLE dither variant everywhere -// • Sweep — ~1.6 seconds where a geometric sweep-front cuts across -// every image panel, carving the old variant away and -// crystallizing the new one behind it. A bright heavy-block -// highlight marks the sweep front at all times. -// -// All image panels share the same phase/timing — synchronized, deliberate, -// momentous. The sweep direction rotates per cycle (horizontal, vertical, -// diagonal, anti-diagonal, radial). - -#[derive(Clone, Copy)] -enum SweepKind { - Horizontal, - Vertical, - Diagonal, - AntiDiag, - Radial, -} - -#[derive(Clone, Copy)] -enum DitherPhase { - Stable { - idx: usize, - }, - Sweep { - from: usize, - to: usize, - t: f32, - kind: SweepKind, - }, -} - -const DITHER_NAMES: &[&str] = &["NONE", "FLOYD", "BAYER", "ATKIN"]; - -/// Convert the global cursor into a dither phase, with a per-zone offset -/// (in seconds) so different image panels run on their own clocks. -/// Asynchronous — each panel transitions when ITS clock says so. -fn dither_phase(cursor: f32, n_variants: usize, offset_secs: f32) -> DitherPhase { - const PERIOD: f32 = 11.0; - const TRANSIT: f32 = 1.6; - let cycle = PERIOD + TRANSIT; - let secs = (cursor / 48.0) + offset_secs; - let cycle_num = secs.div_euclid(cycle) as i64; - let in_cycle = secs.rem_euclid(cycle); - - let kind = match (cycle_num.rem_euclid(5)) as usize { - 0 => SweepKind::Horizontal, - 1 => SweepKind::Vertical, - 2 => SweepKind::Diagonal, - 3 => SweepKind::AntiDiag, - _ => SweepKind::Radial, - }; - - if in_cycle < PERIOD { - DitherPhase::Stable { - idx: (cycle_num.rem_euclid(n_variants as i64)) as usize, - } - } else { - let raw = ((in_cycle - PERIOD) / TRANSIT).clamp(0.0, 1.0); - // Smoothstep — dramatic ease-in/out rather than linear. - let t = raw * raw * (3.0 - 2.0 * raw); - let from = cycle_num.rem_euclid(n_variants as i64) as usize; - let to = (cycle_num + 1).rem_euclid(n_variants as i64) as usize; - DitherPhase::Sweep { from, to, t, kind } - } -} - -/// Sacred glyph cycled per sweep kind — the symbol that marks the moment of -/// transition. Each geometric sweep wears its own sign. -#[inline] -fn sweep_front_glyph(kind: SweepKind) -> char { - match kind { - SweepKind::Horizontal => '✦', // four-pointed star - SweepKind::Vertical => '✧', // outlined star - SweepKind::Diagonal => '◉', // circled dot - SweepKind::AntiDiag => '☸', // wheel of dharma - SweepKind::Radial => '⚘', // flower - } -} - -/// Progress at cell (rx, ry) along the sweep direction, ∈ [0, 1]. -#[inline] -fn sweep_progress(rx: i32, ry: i32, zw: i32, zh: i32, kind: SweepKind) -> f32 { - let zwf = zw.max(1) as f32; - let zhf = zh.max(1) as f32; - match kind { - SweepKind::Horizontal => rx as f32 / zwf, - SweepKind::Vertical => ry as f32 / zhf, - SweepKind::Diagonal => (rx as f32 + (ry as f32) * 2.0) / (zwf + zhf * 2.0), - SweepKind::AntiDiag => ((zwf - rx as f32 - 1.0) + (ry as f32) * 2.0) / (zwf + zhf * 2.0), - SweepKind::Radial => { - let cx = zwf * 0.5; - let cy = zhf * 0.5; - let dx = rx as f32 - cx; - let dy = (ry as f32 - cy) * 2.0; - let d = (dx * dx + dy * dy).sqrt(); - let max_d = ((cx * cx) + (cy * 2.0).powi(2)).sqrt().max(0.01); - d / max_d - } - } -} - -fn paint_image_panel( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - asset_idx: usize, - ctx: &PaintCtx, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - if ctx.assets.is_empty() || asset_idx >= ctx.assets.len() { - paint_fill(grid, zone, zone_id, '·', 0.3); - return; - } - let asset = &ctx.assets[asset_idx]; - if asset.variants.is_empty() { - paint_fill(grid, zone, zone_id, '·', 0.3); - return; - } - let n_variants = asset.variants.len(); - // Per-zone offset (seconds) — each panel runs on its own dither clock. - // tap_offset is a stable per-zone integer; modulate it to seconds. - let zone_offset = (zone.tap_offset as f32 * 0.0019) + zone.pulse * 0.7; - let phase = dither_phase(ctx.cursor, n_variants, zone_offset); - - // Glitch modulation from pulse (still there for texture, not dither). - let pulse_i = (zone.pulse * 3.7) as i32; - let tear_rows: [i32; 3] = [ - ((zone.pulse * 4.2).sin() * zh as f32) as i32 % zh.max(1), - ((zone.pulse * 1.9 + 1.3).sin() * zh as f32) as i32 % zh.max(1), - ((zone.pulse * 2.6 + 3.1).sin() * zh as f32) as i32 % zh.max(1), - ]; - let tear_amts: [i32; 3] = [ - ((zone.pulse * 5.3).sin() * 6.0) as i32, - ((zone.pulse * 3.8 + 0.7).sin() * 4.0) as i32, - ((zone.pulse * 2.1 + 2.4).sin() * 8.0) as i32, - ]; - - let base_stream: i64 = (ctx.cursor as i64) - (zone.tap_offset as i64); - - // Width of the bright sweep front as a fraction of the total sweep distance. - const FRONT_BAND: f32 = 0.035; - - // Native-resolution scrolling: image is sampled 1:1 from braille cells - // and wraps modularly. The frame stays fixed; the image scrolls inside - // it. No warping — aspect ratio preserved. - let scroll_x = (zone.pulse * 1.7) as i32; - let scroll_y = (zone.pulse * 0.9) as i32; - - for ry in 0..zh { - let mut tear_dx = 0_i32; - for k in 0..3 { - if ry == tear_rows[k] { - tear_dx = tear_amts[k]; - } - } - - for rx in 0..zw { - // Decide which variant owns this cell, + whether this cell is - // currently ON the sweep front (gets a bright highlight). - let (v_idx, on_front, front_kind) = match phase { - DitherPhase::Stable { idx } => (idx, false, SweepKind::Horizontal), - DitherPhase::Sweep { from, to, t, kind } => { - let p = sweep_progress(rx, ry, zw, zh, kind); - let front = (p - t).abs() < FRONT_BAND; - let idx = if p < t { to } else { from }; - (idx, front, kind) - } - }; - let variant = &asset.variants[v_idx.min(n_variants - 1)]; - - let vw = variant.w as i32; - let vh = variant.h as i32; - let srx = (rx + tear_dx + scroll_x).rem_euclid(vw.max(1)) as usize; - let sry = (ry + scroll_y).rem_euclid(vh.max(1)) as usize; - let img_ch = variant - .cells - .get(sry) - .and_then(|row| row.get(srx)) - .copied() - .unwrap_or(' '); - - let h = ihash(rx, ry, pulse_i); - let (ch, intensity, fg_override) = if on_front { - // Sweep front — sacred glyph marks the moment of transition. - (sweep_front_glyph(front_kind), 1.0, Some((255, 50, 50))) - } else if h & 0x7f == 0 { - let blocks: &[char] = &['█', '▓', '▒', '░']; - (blocks[(h as usize >> 7) % blocks.len()], 1.0, None) - } else if h & 0x3f == 0 { - let sc = sample(ctx.stream, base_stream + (ry as i64) * 7 + rx as i64); - (sc, 0.85, None) - } else if img_ch == '\u{2800}' { - (' ', 0.08, None) - } else { - (img_ch, 0.92, None) - }; - let fg = fg_override.unwrap_or(color); - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - fg, - intensity, - zone_id, - ); - } - } - - // Top-left: asset name. - let label = asset.name; - for (i, ch) in label.chars().enumerate() { - let lx = zone.rect.x + 1 + i as i32; - if i as i32 + 1 < zw { - put(grid, lx, zone.rect.y, ch, (255, 50, 50), 1.0, zone_id); - } - } - // Bottom-right: dither phase tag — calculated readout of state. - let tag: String = match phase { - DitherPhase::Stable { idx } => { - format!("[{}]", DITHER_NAMES[idx.min(DITHER_NAMES.len() - 1)]) - } - DitherPhase::Sweep { from, to, .. } => format!( - "{}→{}", - DITHER_NAMES[from.min(DITHER_NAMES.len() - 1)], - DITHER_NAMES[to.min(DITHER_NAMES.len() - 1)], - ), - }; - let tag_y = zone.rect.y + zh - 1; - let tag_x_start = zone.rect.x + zw - (tag.chars().count() as i32) - 1; - for (i, ch) in tag.chars().enumerate() { - let lx = tag_x_start + i as i32; - if lx >= zone.rect.x && lx < zone.rect.x + zw { - put(grid, lx, tag_y, ch, (255, 50, 50), 1.0, zone_id); - } - } -} - -/// 13. RaytraceCube — wireframe cube on BLACK background. Built from 8 -/// corners + 12 edges, rotated via raytracer's yaw/pitch helper. Lines -/// rasterized with Bresenham. Zero mask, zero pipes — pure geometry in -/// the middle of the chaos. -fn paint_raytrace_cube(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let zw = zone.rect.w; - let zh = zone.rect.h; - if zw < 6 || zh < 4 { - paint_fill(grid, zone, zone_id, ' ', 0.05); - return; - } - - // Fill zone with true black bg — claim ownership at low positive intensity. - for ry in 0..zh { - for rx in 0..zw { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ' ', - (0, 0, 0), - 0.02, - zone_id, - ); - } - } - - // 8 corners of a unit cube centered at origin. - let corners = [ - Vector3::new(-1.0, -1.0, -1.0), - Vector3::new(1.0, -1.0, -1.0), - Vector3::new(1.0, 1.0, -1.0), - Vector3::new(-1.0, 1.0, -1.0), - Vector3::new(-1.0, -1.0, 1.0), - Vector3::new(1.0, -1.0, 1.0), - Vector3::new(1.0, 1.0, 1.0), - Vector3::new(-1.0, 1.0, 1.0), - ]; - let edges: [(usize, usize); 12] = [ - (0, 1), - (1, 2), - (2, 3), - (3, 0), // back face - (4, 5), - (5, 6), - (6, 7), - (7, 4), // front face - (0, 4), - (1, 5), - (2, 6), - (3, 7), // connecting edges - ]; - - let yaw = zone.pulse * 0.55; - let pitch = (zone.pulse * 0.37).sin() * 0.45; - let roll = (zone.pulse * 0.22).cos() * 0.25; - - // Project each rotated corner to zone cell coords. - let zwf = zw as f32; - let zhf = zh as f32; - let cam_z = 3.2_f32; - let viewport_w = 2.8_f32; - let viewport_h = 2.8_f32; - - let projected: [Option<(i32, i32)>; 8] = { - let mut out = [None; 8]; - for (i, &c) in corners.iter().enumerate() { - let r = rotate_vec_yaw_pitch_roll(c, yaw, pitch, roll); - let z = r.z - cam_z; - if z >= -0.01 { - continue; - } // behind camera - let px = r.x * (-1.0 / z) / (viewport_w * 0.5); - let py = r.y * (-1.0 / z) / (viewport_h * 0.5); - let cx = (px + 1.0) * 0.5 * zwf; - let cy = (1.0 - (py + 1.0) * 0.5) * zhf * 2.0; // aspect correction - let cy = cy * 0.5; - out[i] = Some((cx.round() as i32, cy.round() as i32)); - } - out - }; - - // Draw edges via Bresenham. Back edges (those with any corner having - // a more negative rotated z) rendered with thinner chars — cheap hidden-line hint. - for &(a, b) in &edges { - if let (Some((x0, y0)), Some((x1, y1))) = (projected[a], projected[b]) { - let dx = (x1 - x0).abs(); - let dy = -(y1 - y0).abs(); - let sx = if x0 < x1 { 1 } else { -1 }; - let sy = if y0 < y1 { 1 } else { -1 }; - let mut err = dx + dy; - let (mut x, mut y) = (x0, y0); - loop { - if x >= 0 && x < zw && y >= 0 && y < zh { - put( - grid, - zone.rect.x + x, - zone.rect.y + y, - '█', - (220, 35, 35), - 1.0, - zone_id, - ); - } - if x == x1 && y == y1 { - break; - } - let e2 = 2 * err; - if e2 >= dy { - err += dy; - x += sx; - } - if e2 <= dx { - err += dx; - y += sy; - } - } - } - } -} - -// ─────────────── betting UI formations ─────────────── - -fn paint_box_border(grid: &mut [Vec], zone: &Zone, zone_id: u16, intensity: f32) { - let zw = zone.rect.w; - let zh = zone.rect.h; - if zw < 2 || zh < 2 { - return; - } - for x in 1..zw - 1 { - put( - grid, - zone.rect.x + x, - zone.rect.y, - '─', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + x, - zone.rect.y + zh - 1, - '─', - UI_WHITE, - intensity, - zone_id, - ); - } - for y in 1..zh - 1 { - put( - grid, - zone.rect.x, - zone.rect.y + y, - '│', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + y, - '│', - UI_WHITE, - intensity, - zone_id, - ); - } - put( - grid, - zone.rect.x, - zone.rect.y, - '┌', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y, - '┐', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x, - zone.rect.y + zh - 1, - '└', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + zh - 1, - '┘', - UI_WHITE, - intensity, - zone_id, - ); -} - -fn put_str( - grid: &mut [Vec], - x: i32, - y: i32, - s: &str, - color: (u8, u8, u8), - i: f32, - oid: u16, - max_x: i32, -) { - let mut cx = x; - for ch in s.chars() { - if cx >= max_x { - break; - } - put(grid, cx, y, ch, color, i, oid); - cx += 1; - } -} - -/// Atm — top-corner balance display. -fn paint_atm(grid: &mut [Vec], zone: &Zone, zone_id: u16, balance: u32, cursor: f32) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 1.0); - let ix = zone.rect.x + 2; - let max_x = zone.rect.x + zone.rect.w - 1; - put_str( - grid, - ix, - zone.rect.y + 1, - "ATM ::", - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - let bal = format!("$ {:08}", balance); - put_str( - grid, - ix, - zone.rect.y + 2, - &bal, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - // Tiny cursor indicator - let blink = ((cursor * 0.5) as i32) % 2 == 0; - if blink && zone.rect.h >= 4 { - put_str( - grid, - ix, - zone.rect.y + 3, - "● ONLINE", - (255, 90, 90), - 1.0, - zone_id, - max_x, - ); - } else if zone.rect.h >= 4 { - put_str( - grid, - ix, - zone.rect.y + 3, - "○ ONLINE", - (200, 60, 60), - 0.9, - zone_id, - max_x, - ); - } -} - -/// AgentSlot — labeled "ENTER AGENT" panel for a single player. -fn paint_agent_slot( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - player: u8, - scene_input: &str, -) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 0.9); - let ix = zone.rect.x + 2; - let max_x = zone.rect.x + zone.rect.w - 1; - let label = if player == 0 { "AGENT P1" } else { "AGENT P2" }; - put_str( - grid, - ix, - zone.rect.y + 1, - label, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - let prompt = "[ENTER AGENT >]"; - put_str( - grid, - ix, - zone.rect.y + 2, - prompt, - (255, 110, 110), - 1.0, - zone_id, - max_x, - ); - // Show typed input under P1 only (one buffer for the demo). - if player == 0 && !scene_input.is_empty() && zone.rect.h >= 4 { - let truncated: String = scene_input - .chars() - .take((zone.rect.w - 4) as usize) - .collect(); - put_str( - grid, - ix, - zone.rect.y + 3, - &truncated, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - } -} - -/// Live chess board — converts shakmaty position to braille via dotmax::chess. -/// During global dither sweeps, a sacred sweep-front cuts across the board so -/// the chess "dithers into" the center in lockstep with the image panels. -fn paint_chess_board( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - pos: &Chess, - cursor: f32, -) { - paint_fill(grid, zone, zone_id, ' ', 0.04); - let opts = RenderOptions { - target_width: Some(zone.rect.w as usize), - target_height: Some(zone.rect.h as usize), - ..Default::default() - }; - if let Ok(braille_grid) = render_position_with_options(pos, &opts) { - let (gw, gh) = braille_grid.dimensions(); - // Determine sweep state for the dither overlay. - let phase = dither_phase(cursor, 4, 0.0); - for ry in 0..zone.rect.h.min(gh as i32) { - for rx in 0..zone.rect.w.min(gw as i32) { - let ch = braille_grid.get_char(rx as usize, ry as usize); - let (color, intensity) = if ch == '\u{2800}' || ch == ' ' { - ((50, 5, 5), 0.30) - } else { - (UI_WHITE, 1.0) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - intensity, - zone_id, - ); - } - } - // Chess "dithers in" — sweep front overlay during transitions. - if let DitherPhase::Sweep { t, kind, .. } = phase { - let zw = zone.rect.w; - let zh = zone.rect.h; - for ry in 0..zh { - for rx in 0..zw { - let p = sweep_progress(rx, ry, zw, zh, kind); - if (p - t).abs() < 0.04 { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - sweep_front_glyph(kind), - UI_WHITE, - 1.0, - zone_id, - ); - } - } - } - } - } -} - -/// Big "[ CASH OUT ]" payout button. Solid block frame, white centered text. -fn paint_payout_button(grid: &mut [Vec], zone: &Zone, zone_id: u16, cursor: f32) { - let zw = zone.rect.w; - let zh = zone.rect.h; - paint_fill(grid, zone, zone_id, ' ', 0.0); - // Solid block border - for x in 0..zw { - put( - grid, - zone.rect.x + x, - zone.rect.y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - put( - grid, - zone.rect.x + x, - zone.rect.y + zh - 1, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - } - for y in 0..zh { - put( - grid, - zone.rect.x, - zone.rect.y + y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - } - // Pulsing label - let text = "[ CASH OUT ]"; - let len = text.chars().count() as i32; - let mid_y = zone.rect.y + zh / 2; - let mid_x = zone.rect.x + (zw - len) / 2; - let pulse = ((cursor * 0.05).sin() + 1.0) * 0.5; // 0..1 - let color = ( - 255, - (60.0 + pulse * 60.0) as u8, - (60.0 + pulse * 60.0) as u8, - ); - put_str( - grid, - mid_x, - mid_y, - text, - color, - 1.0, - zone_id, - zone.rect.x + zw - 1, - ); -} - -/// Live terminal input — prompt + buffer + blinking cursor. -fn paint_terminal_input( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - buffer: &str, - cursor: f32, -) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 0.9); - let ix = zone.rect.x + 2; - let mid_y = zone.rect.y + zone.rect.h / 2; - let max_x = zone.rect.x + zone.rect.w - 1; - let prompt = "INPUT> "; - put_str(grid, ix, mid_y, prompt, UI_WHITE, 1.0, zone_id, max_x); - let mut bx = ix + prompt.chars().count() as i32; - for ch in buffer.chars() { - if bx >= max_x - 1 { - break; - } - put(grid, bx, mid_y, ch, UI_WHITE, 1.0, zone_id); - bx += 1; - } - // Blinking cursor block - let blink = ((cursor / 24.0) as i32) % 2 == 0; - if blink && bx < max_x { - put(grid, bx, mid_y, '█', UI_WHITE, 1.0, zone_id); - } -} - -// ─────────────── formation dispatch ─────────────── - -fn paint_formation(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &PaintCtx) { - match zone.formation { - Formation::Raytrace => paint_raytrace(grid, zone, zone_id), - Formation::BlockStrata => paint_block_strata(grid, zone, zone_id), - Formation::ParseDump => paint_parse_dump(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::RegisterDump => paint_register_dump(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::TextmarkConverter => paint_textmark(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::Cellular1D { rule } => { - paint_cellular(grid, zone, zone_id, rule, ctx.stream, ctx.cursor) - } - Formation::Marquee => paint_marquee(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::DensityGrid => paint_density_grid(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::AttentionMatrix => paint_attention(grid, zone, zone_id), - Formation::ProbField => paint_prob_field(grid, zone, zone_id, ctx), - Formation::ImagePanel { asset } => paint_image_panel(grid, zone, zone_id, asset, ctx), - Formation::RaytraceCube => paint_raytrace_cube(grid, zone, zone_id), - Formation::Atm => paint_atm(grid, zone, zone_id, ctx.balance, ctx.cursor), - Formation::AgentSlot { player } => { - paint_agent_slot(grid, zone, zone_id, player, ctx.input_buffer) - } - Formation::ChessBoard => paint_chess_board(grid, zone, zone_id, ctx.chess_pos, ctx.cursor), - Formation::PayoutButton => paint_payout_button(grid, zone, zone_id, ctx.cursor), - Formation::TerminalInput => { - paint_terminal_input(grid, zone, zone_id, ctx.input_buffer, ctx.cursor) - } - } -} - -/// Compression operator applied by a pipe as chars flow through it. -#[derive(Clone, Copy)] -enum Transform { - /// XOR each byte with the key. - Xor(u8), - /// ROT-N on ASCII letters (N = signed shift). - Rot(i8), - /// Encode low nibble as a hex digit. - HexEncode, - /// Reverse byte bits. - BitRev, - /// Keep every other char, drop the rest to '|'. - Stripe, -} - -fn pick_transform() -> Transform { - match r_u32() % 5 { - 0 => Transform::Xor(0x33 + ((r_u32() as u8) & 0x7F)), - 1 => Transform::Rot((1 + (r_u32() % 25)) as i8), - 2 => Transform::HexEncode, - 3 => Transform::BitRev, - _ => Transform::Stripe, - } -} - -fn apply_transform(c: char, t: Transform) -> char { - match t { - Transform::Xor(k) => { - if c.is_ascii() { - let b = (c as u8) ^ k; - if (b as char).is_ascii_graphic() { - b as char - } else { - HEX[(b & 0x0f) as usize] - } - } else { - HEX[((c as u32) & 0x0f) as usize] - } - } - Transform::Rot(n) => { - if c.is_ascii_alphabetic() { - let base = if c.is_ascii_uppercase() { b'A' } else { b'a' }; - let shifted = (((c as u8 - base) as i16 + n as i16).rem_euclid(26)) as u8; - (base + shifted) as char - } else { - c - } - } - Transform::HexEncode => HEX[((c as u32) & 0x0f) as usize], - Transform::BitRev => { - if c.is_ascii() { - let mut b = c as u8; - b = (b >> 4) | (b << 4); - b = ((b >> 2) & 0x33) | ((b << 2) & 0xcc); - b = ((b >> 1) & 0x55) | ((b << 1) & 0xaa); - if (b as char).is_ascii_graphic() { - b as char - } else { - HEX[(b & 0x0f) as usize] - } - } else { - HEX[((c as u32) & 0x0f) as usize] - } - } - Transform::Stripe => { - if (c as u32) & 1 == 0 { - '|' - } else { - c - } - } - } -} - -/// Operator zone symbols — drawn in the middle of every pipe to identify -/// the compression happening inline. Variable-length: 2 or 3 chars. -fn transform_symbols(t: Transform) -> [char; 3] { - match t { - Transform::Xor(k) => ['⊕', HEX[((k >> 4) & 0xf) as usize], HEX[(k & 0xf) as usize]], - Transform::Rot(n) => { - let mag = (n.unsigned_abs() as usize) % 26; - ['↻', HEX[(mag / 16) as usize], HEX[(mag % 16) as usize]] - } - Transform::HexEncode => ['#', '1', '6'], - Transform::BitRev => ['⊥', '↔', '⊥'], - Transform::Stripe => ['▮', '|', '▮'], - } -} - -/// Pipe: an active conduit between two zones. Cells run from inside the source -/// zone, across the shared border, into the destination zone. Along the pipe, -/// chars pass through three zones of painting: -/// [INPUT: raw source chars] → [OPERATOR: transform symbol] → [OUTPUT: transformed] -/// The whole composition becomes a compression machine — each pipe a stage. -struct Pipe { - from: u16, - to: u16, - cells: Vec<(i32, i32)>, // ordered source-end → dest-end - transform: Transform, - step: i64, // stream-chars between consecutive pipe cells (1..=4) -} - -/// Try to build a pipe between two adjacent zones. Returns None if they -/// aren't adjacent or the shared edge is too short to carry a useful pipe. -fn try_build_pipe(zones: &[Zone], i: usize, j: usize) -> Option { - let a = zones[i].base_rect; - let b = zones[j].base_rect; - let min_edge = 4; - let len_each = 5; // cells extending into each zone from the shared border - - // Helper to finish the Pipe once `cells` are built - let mk = |from: u16, to: u16, cells: Vec<(i32, i32)>| -> Pipe { - Pipe { - from, - to, - cells, - transform: pick_transform(), - step: 1 + (r_u32() % 4) as i64, // per-pipe flow granularity - } - }; - - // A-right touches B-left (flow rightwards: from A into B) - if a.x + a.w == b.x { - let y0 = a.y.max(b.y); - let y1 = (a.y + a.h).min(b.y + b.h); - if y1 - y0 < min_edge { - return None; - } - let y = y0 + (y1 - y0) / 2; - let l_a = len_each.min(a.w - 1).max(2); - let l_b = len_each.min(b.w - 1).max(2); - let cells: Vec<(i32, i32)> = ((a.x + a.w - l_a)..(b.x + l_b)).map(|x| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(i as u16, j as u16, cells)); - } - } - // B-right touches A-left (flow rightwards: from B into A) - if b.x + b.w == a.x { - let y0 = a.y.max(b.y); - let y1 = (a.y + a.h).min(b.y + b.h); - if y1 - y0 < min_edge { - return None; - } - let y = y0 + (y1 - y0) / 2; - let l_a = len_each.min(a.w - 1).max(2); - let l_b = len_each.min(b.w - 1).max(2); - let cells: Vec<(i32, i32)> = ((b.x + b.w - l_b)..(a.x + l_a)).map(|x| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(j as u16, i as u16, cells)); - } - } - // A-bottom touches B-top (flow downwards: from A into B) - if a.y + a.h == b.y { - let x0 = a.x.max(b.x); - let x1 = (a.x + a.w).min(b.x + b.w); - if x1 - x0 < min_edge { - return None; - } - let x = x0 + (x1 - x0) / 2; - let l_a = 4.min(a.h - 1).max(2); - let l_b = 4.min(b.h - 1).max(2); - let cells: Vec<(i32, i32)> = ((a.y + a.h - l_a)..(b.y + l_b)).map(|y| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(i as u16, j as u16, cells)); - } - } - // B-bottom touches A-top (flow downwards: from B into A) - if b.y + b.h == a.y { - let x0 = a.x.max(b.x); - let x1 = (a.x + a.w).min(b.x + b.w); - if x1 - x0 < min_edge { - return None; - } - let x = x0 + (x1 - x0) / 2; - let l_a = 4.min(a.h - 1).max(2); - let l_b = 4.min(b.h - 1).max(2); - let cells: Vec<(i32, i32)> = ((b.y + b.h - l_b)..(a.y + l_a)).map(|y| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(j as u16, i as u16, cells)); - } - } - None -} - -fn build_pipes(zones: &[Zone]) -> Vec { - // Build a pipe between EVERY adjacent (non-Nested) pair that admits one. - // The whole screen becomes visibly networked. - let mut pipes = Vec::new(); - for i in 0..zones.len() { - if zones[i].side == Side::Nested { - continue; - } - for j in (i + 1)..zones.len() { - if zones[j].side == Side::Nested { - continue; - } - if let Some(p) = try_build_pipe(zones, i, j) { - pipes.push(p); - } - } - } - pipes -} - -/// Streamer axis — direction of a persistent overlay flow line. -#[derive(Clone, Copy)] -enum StreamerAxis { - Horizontal, - Vertical, - DiagPos, - DiagNeg, -} - -/// A single persistent overlay flow line. -struct Streamer { - axis: StreamerAxis, - anchor: i32, // y for H, x for V, intercept for diagonals (top edge) - speed: f32, // chars / sec - direction: i32, // +1 / -1 — flow direction along the line -} - -fn streamer_cells(axis: StreamerAxis, anchor: i32, w: i32, h: i32) -> Vec<(i32, i32)> { - match axis { - StreamerAxis::Horizontal => (0..w).map(|x| (x, anchor)).collect(), - StreamerAxis::Vertical => (0..h).map(|y| (anchor, y)).collect(), - StreamerAxis::DiagPos => { - let mut out = Vec::new(); - let mut x = anchor; - let mut y = 0; - while y < h { - if x >= 0 && x < w { - out.push((x, y)); - } - y += 1; - x += 2; // step 2 cells horizontally per row → ~45° on screen aspect - } - out - } - StreamerAxis::DiagNeg => { - let mut out = Vec::new(); - let mut x = anchor; - let mut y = 0; - while y < h { - if x >= 0 && x < w { - out.push((x, y)); - } - y += 1; - x -= 2; - } - out - } - } -} - -#[inline] -fn rect_contains(r: Rect, x: i32, y: i32) -> bool { - x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h -} - -fn paint_streamer( - grid: &mut [Vec], - s: &Streamer, - w: i32, - h: i32, - protected: &[Rect], - stream: &[char], - cursor: f32, -) { - let cells = streamer_cells(s.axis, s.anchor, w, h); - let scroll = (cursor * s.speed) as i64; - for (i, &(x, y)) in cells.iter().enumerate() { - if protected.iter().any(|r| rect_contains(*r, x, y)) { - continue; - } - let pos = scroll + (i as i64) * s.direction as i64; - let ch = sample(stream, pos); - put_force(grid, x, y, ch, (250, 55, 55), 0.95); - } -} - -/// Edge-anchored noise injection. Emits a short trail of chars from an edge -/// point inward, scrolling with its own speed. Different from the global hose. -struct NoiseFeed { - pos: (i32, i32), // edge cell - dir: (i32, i32), // (dx, dy) inward unit vector - length: i32, // trail length in cells - seed: u32, // unique noise seed per feed - speed: f32, // chars / sec -} - -const NOISE_POOL: &[char] = &[ - '#', '@', '%', '$', '*', '!', '?', '&', '+', '=', '~', '^', '\\', -]; - -fn paint_noise_feed(grid: &mut [Vec], f: &NoiseFeed, protected: &[Rect], cursor: f32) { - let scroll = (cursor * f.speed) as u32; - for i in 0..f.length { - let x = f.pos.0 + f.dir.0 * i; - let y = f.pos.1 + f.dir.1 * i; - if protected.iter().any(|r| rect_contains(*r, x, y)) { - continue; - } - let h = f - .seed - .wrapping_mul(2_654_435_761) - .wrapping_add(scroll.wrapping_mul(31)) - .wrapping_add(i as u32); - let ch = NOISE_POOL[(h as usize) % NOISE_POOL.len()]; - let trail_fade = 1.0 - (i as f32) / (f.length.max(1) as f32); - let intensity = 0.55 + trail_fade * 0.40; - put_force(grid, x, y, ch, (245, 50, 50), intensity); - } -} - -/// A wandering "dither worm" — a moving point that leaves a fading trail of -/// block-density chars (█▓▒░) across the screen. Ambulates through whatever's -/// there, painting density on top. Bounces off edges, slowly turns at random. -struct DitherFlow { - pos_x: f32, - pos_y: f32, - vel_x: f32, - vel_y: f32, - trail: Vec<(i32, i32)>, - trail_max: usize, -} - -const FLOW_RAMP: &[char] = &['░', '▒', '▓', '█']; - -fn tick_flow(flow: &mut DitherFlow, dt: f32, w: i32, h: i32) { - flow.pos_x += flow.vel_x * dt; - flow.pos_y += flow.vel_y * dt; - if flow.pos_x < 0.0 { - flow.pos_x = 0.0; - flow.vel_x = flow.vel_x.abs(); - } else if flow.pos_x >= w as f32 { - flow.pos_x = (w - 1) as f32; - flow.vel_x = -flow.vel_x.abs(); - } - if flow.pos_y < 1.0 { - flow.pos_y = 1.0; - flow.vel_y = flow.vel_y.abs(); - } else if flow.pos_y >= h as f32 { - flow.pos_y = (h - 1) as f32; - flow.vel_y = -flow.vel_y.abs(); - } - // Slow random wander — rotate velocity vector by a small angle. - if r_f32() < 0.07 { - let theta = (r_f32() - 0.5) * 0.6; - let (ct, st) = (theta.cos(), theta.sin()); - let nvx = flow.vel_x * ct - flow.vel_y * st; - let nvy = flow.vel_x * st + flow.vel_y * ct; - flow.vel_x = nvx; - flow.vel_y = nvy; - } - let cell = (flow.pos_x as i32, flow.pos_y as i32); - if flow.trail.last() != Some(&cell) { - flow.trail.push(cell); - if flow.trail.len() > flow.trail_max { - flow.trail.remove(0); - } - } -} - -fn paint_flow(grid: &mut [Vec], flow: &DitherFlow) { - let n = flow.trail.len(); - if n == 0 { - return; - } - for (i, &(x, y)) in flow.trail.iter().enumerate() { - // 0 = oldest (dimmest, lightest density char) → n-1 = head (brightest, █). - let age_pct = i as f32 / n as f32; - let level = ((age_pct * FLOW_RAMP.len() as f32) as usize).min(FLOW_RAMP.len() - 1); - let intensity = 0.45 + age_pct * 0.55; - put_force(grid, x, y, FLOW_RAMP[level], (255, 70, 70), intensity); - } -} - -/// A short-lived chunk of an image (or the whole thing) blasted onto the -/// screen at a random rect, force-painted over everything. Lives ~0.5–3 sec. -struct GlitchInsertion { - asset_idx: usize, - rect: Rect, - /// Optional sub-rectangle of the asset to sample from. None = full asset. - crop: Option, - spawn_cursor: f32, - duration_chars: f32, - /// Which dither variant to render. Doesn't have to match anything else. - variant_idx: usize, -} - -fn paint_glitch_insertion( - grid: &mut [Vec], - ins: &GlitchInsertion, - assets: &[ImageAsset], - cursor: f32, -) { - if ins.asset_idx >= assets.len() { - return; - } - let asset = &assets[ins.asset_idx]; - if asset.variants.is_empty() { - return; - } - let variant = &asset.variants[ins.variant_idx.min(asset.variants.len() - 1)]; - - let vw = variant.w as i32; - let vh = variant.h as i32; - // crop.x/.y is the source offset where this insertion's (0,0) maps to. - // Native 1:1 sampling — image scrolls/crops, never warps. - let off_x = ins.crop.map(|c| c.x).unwrap_or(0); - let off_y = ins.crop.map(|c| c.y).unwrap_or(0); - - let age = (cursor - ins.spawn_cursor) / ins.duration_chars.max(0.01); - let life_factor = if age < 0.15 { - age / 0.15 - } else if age > 0.85 { - ((1.0 - age) / 0.15).max(0.0) - } else { - 1.0 - }; - - for ry in 0..ins.rect.h { - for rx in 0..ins.rect.w { - let srx = (off_x + rx).rem_euclid(vw.max(1)) as usize; - let sry = (off_y + ry).rem_euclid(vh.max(1)) as usize; - let ch = variant - .cells - .get(sry) - .and_then(|row| row.get(srx)) - .copied() - .unwrap_or(' '); - if ch == '\u{2800}' || ch == ' ' { - continue; - } - let intensity = (0.7 + 0.3 * life_factor).min(1.0); - put_force( - grid, - ins.rect.x + rx, - ins.rect.y + ry, - ch, - (255, 60, 60), - intensity, - ); - } - } -} - -/// The litany — scripture that runs as a single bright row across the very -/// top of the screen, always above everything else. Overwrites whatever zone -/// owned row 0, unbroken by borders. The creed speaks to the whole room. -const LITANY: &str = - " ✦ ONE CURSOR FLOWS AND ALL CELLS AWAKEN ☸ BY GOLDEN ANGLE ALL THINGS ALIGN \ - ✧ PIPES CARRY WHAT CANNOT BE HELD ◉ φ = 1.618 IS THE ARCHITECT \ - ⚘ SIGNAL BECOMES SACRAMENT ✦ AS ABOVE SO BELOW ☸ \ - THE CUBE AT THE EAST KEEPS COUNT ✧ FOLD BY FOLD ◉ \ - HOSE IS HOLY HOSE IS HOLY HOSE IS HOLY ⚘ "; - -fn paint_litany(grid: &mut [Vec], w: i32, cursor: f32) { - let chars: Vec = LITANY.chars().collect(); - let n = chars.len() as i64; - if n == 0 || grid.is_empty() { - return; - } - let scroll = (cursor * 0.35) as i64; - for x in 0..w { - let idx = (scroll + x as i64).rem_euclid(n) as usize; - put_force(grid, x, 0, chars[idx], (255, 50, 50), 1.0); - } -} - -fn paint_pipe( - grid: &mut [Vec], - pipe: &Pipe, - zones: &[Zone], - stream: &[char], - assets: &[ImageAsset], - cursor: f32, -) { - let from_zone = &zones[pipe.from as usize]; - let from_tap = from_zone.tap_offset as i64; - let n = pipe.cells.len(); - let input_end = n / 3; - let op_end = (n * 2) / 3; - let op_syms = transform_symbols(pipe.transform); - let op_len = op_end - input_end; - - // If the source zone is an ImagePanel, the pipe carries its raw luma bytes - // instead of generic stream chars. You literally see the image's pixel - // data flowing across the compression operator. - let image_src: Option<&[u8]> = match from_zone.formation { - Formation::ImagePanel { asset } => assets.get(asset).map(|a| a.luma.as_slice()), - _ => None, - }; - - // Helper: fetch a char at position `p` along the pipe — either from the - // global stream or from the source image's luma buffer encoded as a hex - // digit pair (so the byte-value shape reads as data). - let fetch_char = |p: i64| -> char { - if let Some(bytes) = image_src { - if bytes.is_empty() { - return ' '; - } - let idx = p.rem_euclid(bytes.len() as i64 * 2) as usize; - let byte = bytes[idx / 2]; - let nib = if idx & 1 == 0 { byte >> 4 } else { byte & 0x0f }; - HEX[nib as usize] - } else { - sample(stream, p) - } - }; - - for (i, &(x, y)) in pipe.cells.iter().enumerate() { - let ch = if i < input_end { - fetch_char((cursor as i64) - from_tap - i as i64 * pipe.step) - } else if i < op_end { - let sym_idx = (i - input_end) * op_syms.len() / op_len.max(1); - op_syms[sym_idx.min(2)] - } else { - let delay = (i - input_end) as i64 * pipe.step; - let src_pos = (cursor as i64) - from_tap - delay; - // Apply transform on the char we'd display at input side. - // For image sources, the char is already a hex digit so transforming - // it gives visible XOR/rot/bit-rev/stripe output, reading as - // "encoded image bytes crossing the operator." - let src = fetch_char(src_pos); - apply_transform(src, pipe.transform) - }; - let i_val = if (input_end..op_end).contains(&i) { - 0.92 - } else { - 1.0 - }; - put_force(grid, x, y, ch, (255, 50, 50), i_val); - } -} - -/// Edge-morph pass — after formations paint, cells within 3 of any zone edge -/// have a probability of bleeding in a character + color from a neighbor-owned -/// cell. Creates a soft, shimmering boundary between adjacent zones where -/// the character "languages" morph into each other. Exempts the cube zone. -fn apply_edge_morph(grid: &mut [Vec], scene: &Scene) { - let grid_h = grid.len() as i32; - let grid_w = if grid.is_empty() { - 0 - } else { - grid[0].len() as i32 - }; - - for i in 0..scene.zones.len() { - let z = &scene.zones[i]; - if matches!(z.formation, Formation::RaytraceCube) { - continue; - } - let r = z.base_rect; - let morph_d: i32 = 3; - let t_phase = (scene.cursor * 0.25) as i32; - - for ry in 0..r.h { - for rx in 0..r.w { - let dx = rx.min(r.w - 1 - rx); - let dy = ry.min(r.h - 1 - ry); - let dist = dx.min(dy); - if dist >= morph_d { - continue; - } - - let gx = r.x + rx; - let gy = r.y + ry; - if gx < 0 || gy < 0 || gx >= grid_w || gy >= grid_h { - continue; - } - let (ugx, ugy) = (gx as usize, gy as usize); - if grid[ugy][ugx].owner != i as u16 { - continue; - } - - // Nearness in [0, 1]; squared so effect falls off faster. - let nearness = (morph_d - dist) as f32 / morph_d as f32; - let h_val = ihash(rx, ry, t_phase); - let r_val = (h_val & 0xff) as f32 / 255.0; - let threshold = 0.55 * nearness * nearness; - if r_val >= threshold { - continue; - } - - // Pick a direction outward — one of 4 cardinal dirs weighted - // toward the nearest edge so bleeding mostly comes from the - // neighbor on that side. - let (sdx, sdy): (i32, i32) = if dx < dy { - if rx < r.w / 2 { - (-1, 0) - } else { - (1, 0) - } - } else { - if ry < r.h / 2 { - (0, -1) - } else { - (0, 1) - } - }; - let steps = 1 + ((h_val >> 8) & 0x3) as i32; - let lx = gx + sdx * steps; - let ly = gy + sdy * steps; - if lx < 0 || ly < 0 || lx >= grid_w || ly >= grid_h { - continue; - } - let src = grid[ly as usize][lx as usize]; - // Only morph if the source is owned by a DIFFERENT zone and - // that zone isn't the cube (cube stays crisp). - if src.owner == i as u16 || src.owner == NO_OWNER { - continue; - } - if matches!( - scene.zones[src.owner as usize].formation, - Formation::RaytraceCube - ) { - continue; - } - - let cell = &mut grid[ugy][ugx]; - cell.ch = src.ch; - cell.fg = src.fg; - // Intensity: blend toward source, preserving some of current. - cell.intensity = (cell.intensity * 0.55 + src.intensity * 0.65).min(1.0); - } - } - } -} - -/// Whether a formation is part of the betting UI overlay (paints LAST so it -/// stays on top of the chaos, but uses normal `put` so chaos can still bleed -/// through cells where its intensity beats the UI's). -fn is_ui_formation(f: &Formation) -> bool { - matches!( - f, - Formation::Atm - | Formation::AgentSlot { .. } - | Formation::ChessBoard - | Formation::PayoutButton - | Formation::TerminalInput - ) -} - -fn render(scene: &Scene) -> Vec> { - let mut grid = vec![vec![PxCell::empty(); scene.w as usize]; scene.h as usize]; - let ctx = PaintCtx { - stream: &scene.stream, - cursor: scene.cursor, - zones: &scene.zones, - adjacency: &scene.adjacency, - assets: &scene.assets, - chess_pos: &scene.chess_pos, - balance: scene.balance, - input_buffer: &scene.input_buffer, - }; - // 1) NON-UI formations first — fib zones, image panels, anything that - // forms the chaotic substrate. - for i in 0..scene.zones.len() { - if !is_ui_formation(&scene.zones[i].formation) { - paint_formation(&mut grid, &scene.zones[i], i as u16, &ctx); - } - } - // 2) Edge-morph pass — zone borders bleed their neighbors' chars in. - apply_edge_morph(&mut grid, scene); - // 3) Pipes force-paint on top — the compression machinery between cells. - for p in &scene.pipes { - paint_pipe( - &mut grid, - p, - &scene.zones, - &scene.stream, - &scene.assets, - scene.cursor, - ); - } - // 4) Persistent overlay streamers (orthogonal + crossed diagonals). - for s in &scene.streamers { - paint_streamer( - &mut grid, - s, - scene.w, - scene.h, - &scene.protected_rects, - &scene.stream, - scene.cursor, - ); - } - // 5) Noise projection feeds from edges. - for f in &scene.noise_feeds { - paint_noise_feed(&mut grid, f, &scene.protected_rects, scene.cursor); - } - // 5b) Dither flow worms — block-density trails ambulating through the grids. - for flow in &scene.dither_flows { - paint_flow(&mut grid, flow); - } - // 6) Glitch insertions — random image fragments blasted on top. - for ins in &scene.glitch_inserts { - paint_glitch_insertion(&mut grid, ins, &scene.assets, scene.cursor); - } - // 7) UI zones (chess + ATM + agents + payout + terminal) re-paint LAST - // so the betting interface stays readable, but chaos leaks through - // every cell where the UI's intensity is below the chaos behind it. - for i in 0..scene.zones.len() { - if is_ui_formation(&scene.zones[i].formation) { - paint_formation(&mut grid, &scene.zones[i], i as u16, &ctx); - } - } - // 8) Litany — scripture scrolling across row 0, above everything. - paint_litany(&mut grid, scene.w, scene.cursor); - // 7) Flip: mirror every row horizontally at the very end so the creed - // flips too — the mirror universe has its own scripture. - if scene.flipped { - for row in grid.iter_mut() { - row.reverse(); - } - } - grid -} - -fn dim(c: (u8, u8, u8), i: f32) -> (u8, u8, u8) { - let f = i.clamp(0.0, 1.0); - ( - (c.0 as f32 * f) as u8, - (c.1 as f32 * f) as u8, - (c.2 as f32 * f) as u8, - ) -} - -fn draw(stdout: &mut impl Write, grid: &[Vec]) -> io::Result<()> { - queue!(stdout, cursor::MoveTo(0, 0))?; - let mut last_fg: Option<(u8, u8, u8)> = None; - for (i, row) in grid.iter().enumerate() { - queue!(stdout, cursor::MoveTo(0, i as u16))?; - for cell in row { - let fg = dim(cell.fg, cell.intensity); - if Some(fg) != last_fg { - queue!( - stdout, - SetForegroundColor(Color::Rgb { - r: fg.0, - g: fg.1, - b: fg.2 - }) - )?; - last_fg = Some(fg); - } - queue!(stdout, Print(cell.ch))?; - } - } - queue!(stdout, ResetColor)?; - stdout.flush()?; - Ok(()) -} - -// ───────────────────────── main ───────────────────────── -fn main() -> io::Result<()> { - let mut stdout = io::stdout(); - terminal::enable_raw_mode()?; - execute!( - stdout, - EnterAlternateScreen, - cursor::Hide, - Clear(ClearType::All) - )?; - - let (cols, rows) = terminal::size()?; - let w = (cols as i32).max(60); - let h = ((rows as i32) - 1).max(12); - let mut scene = build_scene(w, h); - - let target = Duration::from_millis(33); - let mut last = Instant::now(); - - let result = (|| -> io::Result<()> { - loop { - if event::poll(Duration::ZERO)? { - if let Event::Key(k) = event::read()? { - match (k.code, k.modifiers) { - // Always-on quit - (KeyCode::Esc, _) => break, - (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => break, - // Toggles moved to Ctrl-modified so plain f/r are typeable. - (KeyCode::Char('f'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.flipped = !scene.flipped - } - (KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.reversed = !scene.reversed - } - // Backspace edits the live input buffer. - (KeyCode::Backspace, _) => { - scene.input_buffer.pop(); - } - // Enter clears the buffer (treats it as "submit"). - (KeyCode::Enter, _) => { - scene.input_buffer.clear(); - } - // Plain printable chars (no Ctrl) → input buffer. - (KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { - if scene.input_buffer.chars().count() < 30 { - scene.input_buffer.push(c); - } - } - _ => {} - } - } - } - let now = Instant::now(); - let dt = (now - last).as_secs_f32().min(0.1); - last = now; - - tick(&mut scene, dt); - let grid = render(&scene); - draw(&mut stdout, &grid)?; - - let elapsed = last.elapsed(); - if elapsed < target { - std::thread::sleep(target - elapsed); - } - } - Ok(()) - })(); - - execute!(stdout, ResetColor, cursor::Show, LeaveAlternateScreen)?; - terminal::disable_raw_mode()?; - result -} diff --git a/examples/zone_stream_canvas_2.rs b/examples/zone_stream_canvas_2.rs deleted file mode 100644 index 68b4f4c..0000000 --- a/examples/zone_stream_canvas_2.rs +++ /dev/null @@ -1,3466 +0,0 @@ -//! ─── creed of the hose ─── -//! -//! One cursor flows; all cells awaken. -//! At the shared edge, the data bleeds. -//! φ = 1.618 is the architect. 2π · (1 − 1/φ) is the pitch. -//! The cube watches from the east. It keeps count. -//! -//! Every frame, three truths are sung together: -//! formations hold their ground, -//! pipes carry what cannot be held, -//! the sweep-front paints the new in. -//! -//! Press f to invert the world. Press r to unwind it. -//! Press q / Esc to leave the room. -//! -//! Run: cargo run --example zone_stream --release --features "raytracer image" - -use crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyModifiers}, - execute, queue, - style::{Color, Print, ResetColor, SetForegroundColor}, - terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use dotmax::chess::board::{render_position_with_options, RenderOptions}; -use dotmax::image::{DitheringMethod, ImageRenderer}; -use dotmax::raytracer::wireframe::rotate_vec_yaw_pitch_roll; -use dotmax::raytracer::{ - render_with_orientation, Camera, RenderMode, Scene as RtScene, Sphere, Vector3, - WireframeRotation, -}; -use shakmaty::{Chess, Position}; -use std::{ - cell::Cell as StdCell, - io::{self, Write}, - path::Path, - time::{Duration, Instant}, -}; - -// ───────────────────────── tiny xorshift RNG ───────────────────────── -thread_local! { static RNG: StdCell = StdCell::new(0x1234_5678); } -fn r_u32() -> u32 { - RNG.with(|c| { - let mut x = c.get(); - if x == 0 { - x = 0x9E37_79B9; - } - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - c.set(x); - x - }) -} -fn r_f32() -> f32 { - (r_u32() as f32) / (u32::MAX as f32) -} -fn r_pick(xs: &[T]) -> T { - xs[(r_u32() as usize) % xs.len()] -} - -fn seed_from_clock() { - let nanos = Instant::now().elapsed().as_nanos() as u32 - ^ std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.subsec_nanos()) - .unwrap_or(0); - RNG.with(|c| c.set(nanos | 1)); -} - -// ───────────────────────── glyph pools ───────────────────────── -const HEX: &[char] = &[ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', -]; -const BITS: &[char] = &['0', '1']; -const PUNCT: &[char] = &[ - '!', '@', '#', '$', '%', '&', '*', '+', '=', '<', '>', '?', '/', '\\', '^', '~', -]; -const KANA: &[char] = &[ - 'ヲ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ッ', 'ア', 'イ', 'ウ', 'エ', 'オ', 'ハ', 'ヒ', 'フ', 'ヘ', - 'ホ', 'マ', 'ミ', 'ム', -]; -const BLOCK: &[char] = &['░', '▒', '▓', '█', '▚', '▞', '▙', '▟']; -const GREEK: &[char] = &[ - 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'λ', 'μ', 'π', 'σ', 'τ', 'φ', 'ψ', 'ω', -]; - -const SNIPPETS: &[&str] = &[ - " ::SYNC:: ", - " 0xDEAD ", - " [OK] ", - " ROUTINE 0x42 ", - " ACK ", - " FAULT ", - " λ=0x1F ", - " >>> ", - " <<< ", - " /proc/self ", - " alloc= ", - " ACK 0x7F ", - " EOF ", - " NULL ", - " TX/RX ", - " PID:4821 ", - " SIG 0x4A ", - " ENTER ", - " φ=1.618 ", - " √2=1.414 ", - " θ=π/φ ", - " FIB(13)=233 ", - " // BREACH ", - // scripture - " ✦ INVOCATION ✦ ", - " ∴ by golden angle ∴ ", - " one cursor many cells ", - " ☸ hose is holy ☸ ", - " the cube watches ", - " as above so below ", - " // GOLDEN HOUR // ", - " ⚘ signal becomes sacrament ⚘ ", - " ∞ one cursor ∞ ", - " ACK the geometry ", - " fold by fold ", - " ✧ enter be transformed ✧ ", - " ∇ scripture ∇ ", -]; - -// ───────────────────────── stream source ───────────────────────── -fn build_stream(len: usize) -> Vec { - let pools: &[&[char]] = &[HEX, BITS, PUNCT, KANA, BLOCK, GREEK]; - let mut out = Vec::with_capacity(len); - while out.len() < len { - if r_f32() < 0.15 { - let s = SNIPPETS[(r_u32() as usize) % SNIPPETS.len()]; - for ch in s.chars() { - if out.len() >= len { - break; - } - out.push(ch); - } - } else { - let p = pools[(r_u32() as usize) % pools.len()]; - let burst = 4 + (r_u32() as usize) % 9; - for _ in 0..burst { - if out.len() >= len { - break; - } - out.push(r_pick(p)); - } - } - } - out -} - -// ───────────────────────── geometry ───────────────────────── -#[derive(Clone, Copy, Debug)] -struct Rect { - x: i32, - y: i32, - w: i32, - h: i32, -} - -/// Recursive φ-subdivision producing a Fibonacci-style spiral of rects. -/// `cw = true` spirals inward clockwise, `false` counter-clockwise. -fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { - let mut out = Vec::new(); - let mut rect = initial; - const PHI_COMPLEMENT: f32 = 0.381_966; // 1 - 1/φ - - for step in 0..max_depth { - if rect.w < 6 || rect.h < 4 { - break; - } - let vertical_split = rect.w >= rect.h; - // Alternate which side the leaf sits on each step; `cw` flips polarity. - let leaf_far = ((step % 2 == 0) ^ !cw) != false; - - if vertical_split { - let leaf_w = ((rect.w as f32) * PHI_COMPLEMENT).round().max(3.0) as i32; - if leaf_far { - out.push(Rect { - x: rect.x + rect.w - leaf_w, - y: rect.y, - w: leaf_w, - h: rect.h, - }); - rect.w -= leaf_w; - } else { - out.push(Rect { - x: rect.x, - y: rect.y, - w: leaf_w, - h: rect.h, - }); - rect.x += leaf_w; - rect.w -= leaf_w; - } - } else { - let leaf_h = ((rect.h as f32) * PHI_COMPLEMENT).round().max(2.0) as i32; - if leaf_far { - out.push(Rect { - x: rect.x, - y: rect.y + rect.h - leaf_h, - w: rect.w, - h: leaf_h, - }); - rect.h -= leaf_h; - } else { - out.push(Rect { - x: rect.x, - y: rect.y, - w: rect.w, - h: leaf_h, - }); - rect.y += leaf_h; - rect.h -= leaf_h; - } - } - } - out.push(rect); - out -} - -/// Golden-ratio child rect inside `parent` — size = parent × 1/φ, random offset -/// snapped to one of the golden-ratio anchor points. -fn golden_child(parent: Rect) -> Rect { - const INV_PHI: f32 = 0.618_034; - let cw = ((parent.w as f32) * INV_PHI).round().max(4.0) as i32; - let ch = ((parent.h as f32) * INV_PHI).round().max(3.0) as i32; - let cw = cw.min(parent.w - 1); - let ch = ch.min(parent.h - 1); - // Pick a corner bias — 4 golden anchors (φ/1-φ combinations). - let bias_x = if r_f32() < 0.5 { 0.0 } else { 1.0 - INV_PHI }; - let bias_y = if r_f32() < 0.5 { 0.0 } else { 1.0 - INV_PHI }; - let x = parent.x + ((parent.w - cw) as f32 * bias_x).round() as i32; - let y = parent.y + ((parent.h - ch) as f32 * bias_y).round() as i32; - Rect { x, y, w: cw, h: ch } -} - -// ───────────────────────── types ───────────────────────── -#[derive(Clone, Copy, PartialEq, Eq)] -enum Side { - L, - R, - Chaos, - Nested, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum FlowDir { - RowMajor, - RowMajorRev, - ColMajor, - ColMajorRev, -} - -fn random_flow() -> FlowDir { - match r_u32() & 3 { - 0 => FlowDir::RowMajor, - 1 => FlowDir::RowMajorRev, - 2 => FlowDir::ColMajor, - _ => FlowDir::ColMajorRev, - } -} - -#[inline] -fn cell_index(rx: i32, ry: i32, w: i32, h: i32, dir: FlowDir) -> i64 { - let rx = rx as i64; - let ry = ry as i64; - let w = w as i64; - let h = h as i64; - match dir { - FlowDir::RowMajor => ry * w + rx, - FlowDir::RowMajorRev => (h - 1 - ry) * w + (w - 1 - rx), - FlowDir::ColMajor => rx * h + ry, - FlowDir::ColMajorRev => (w - 1 - rx) * h + (h - 1 - ry), - } -} - -/// Bespoke algorithms. Each zone becomes a compressed/algorithmic/text signal -/// expression — no smooth waves or lissajous curves, just chunky block/text art. -#[derive(Clone, Copy)] -enum Formation { - /// Spinning wireframe sphere raytraced into the zone as an intensity ramp. - Raytrace, - /// Hard-stepped {░▒▓█} strata scrolling vertically. - BlockStrata, - /// Hex memory dump: `xxxx: AB CD EF ...` - ParseDump, - /// Named registers with values: `R0: 0x4FE8D1A2` - RegisterDump, - /// Stream char → arrow → ROT13 / nibble transform. - TextmarkConverter, - /// Elementary CA (rule 30 or 110) seeded from stream bits. - Cellular1D { rule: u8 }, - /// Scrolling stream text with bright middle band, dim above/below. - Marquee, - /// 2-cell-block density mosaic from stream bytes. - DensityGrid, - /// Sparse transformer-attention pattern — diagonal band + sink cols + hotspots. - AttentionMatrix, - /// Probability distribution over candidate next tokens — top-K bars, - /// sorted by mass, values derived from stream. This is literally what a - /// language model *is* at any moment — a distribution. - ProbField, - /// Converted image (tiger/viper/etc) rendered via full dotmax pipeline - /// (ImageRenderer → BrailleGrid). Subject to glitching effects. - ImagePanel { asset: usize }, - /// Wireframe cube on BLACK background — kept for future use. - #[allow(dead_code)] - RaytraceCube, - /// ATM panel — top-corner balance display with bordered frame. - Atm, - /// "ENTER AGENT" slot — labeled panel for one player. - AgentSlot { player: u8 }, - /// Live chess board rendered via dotmax::chess from a shakmaty position. - ChessBoard, - /// Big "[ CASH OUT ]" payout button. - PayoutButton, - /// Live text input — captures typing, shows prompt + buffer + cursor. - TerminalInput, -} - -/// Important UI text (labels, balance digits, button text) is bright WHITE -/// so it stands out from the deep-red field. Use this for any UI chrome -/// that absolutely must read clearly. -const UI_WHITE: (u8, u8, u8) = (255, 255, 255); - -/// A single dither-variant render of an image. -struct ImageVariant { - cells: Vec>, - w: usize, - h: usize, -} - -/// An image with multiple dither variants pre-rendered. Paint time picks -/// a variant per-cell via a wave function so different dither styles -/// sweep across the image over time. -struct ImageAsset { - name: &'static str, - variants: Vec, // one per dither method - luma: Vec, // raw pattern bytes from first variant — pipe payload -} - -const DITHER_METHODS: &[DitheringMethod] = &[ - DitheringMethod::None, - DitheringMethod::FloydSteinberg, - DitheringMethod::Bayer, - DitheringMethod::Atkinson, -]; - -/// Load and convert one image, rendering every dither method in -/// DITHER_METHODS as separate variants. -fn load_image( - path: &str, - name: &'static str, - cells_w: usize, - cells_h: usize, -) -> Option { - let mut variants: Vec = Vec::with_capacity(DITHER_METHODS.len()); - let mut luma: Option> = None; - for &m in DITHER_METHODS { - let grid = ImageRenderer::new() - .load_from_path(Path::new(path)) - .ok()? - .resize(cells_w, cells_h, true) - .ok()? - .dithering(m) - .render() - .ok()?; - let (gw, gh) = grid.dimensions(); - let mut cells: Vec> = vec![vec![' '; gw]; gh]; - for y in 0..gh { - for x in 0..gw { - cells[y][x] = grid.get_char(x, y); - } - } - if luma.is_none() { - luma = Some(grid.get_raw_patterns().to_vec()); - } - variants.push(ImageVariant { - cells, - w: gw, - h: gh, - }); - } - Some(ImageAsset { - name, - variants, - luma: luma.unwrap_or_default(), - }) -} - -fn load_image_assets() -> Vec { - // Heavy on tigers, snakes, rabbits. A little frog. Some grifter. - let candidates: &[(&str, &str, &'static str)] = &[ - ( - "tests/fixtures/images/tiger_small.png", - "./tests/fixtures/images/tiger_small.png", - "TIGER", - ), - ( - "tests/fixtures/images/tiger_1.png", - "./tests/fixtures/images/tiger_1.png", - "TIGR2", - ), - ( - "tests/fixtures/images/viper3.png", - "./tests/fixtures/images/viper3.png", - "VIPER", - ), - ( - "tests/fixtures/images/viper_head_3.png", - "./tests/fixtures/images/viper_head_3.png", - "VHEAD", - ), - ( - "tests/fixtures/images/extras/snakedesk.png", - "./tests/fixtures/images/extras/snakedesk.png", - "SNAKE", - ), - ( - "tests/fixtures/images/extras/rabbit.png", - "./tests/fixtures/images/extras/rabbit.png", - "RABT", - ), - ( - "tests/fixtures/images/extras/grifter.jpg", - "./tests/fixtures/images/extras/grifter.jpg", - "GRFTR", - ), - ( - "tests/fixtures/images/extras/frog_01.png", - "./tests/fixtures/images/extras/frog_01.png", - "FROG", - ), - ( - "tests/fixtures/images/extras/frog_02.png", - "./tests/fixtures/images/extras/frog_02.png", - "FROG2", - ), - ]; - let mut out = Vec::new(); - for &(p1, p2, name) in candidates { - if let Some(a) = load_image(p1, name, 64, 32).or_else(|| load_image(p2, name, 64, 32)) { - out.push(a); - } - } - out -} - -fn pick_formation(rect: Rect) -> Formation { - let aspect = (rect.w as f32) / (rect.h.max(1) as f32); - let r = r_u32() as usize; - if aspect > 3.5 { - // Very wide — horizontal readouts. - match r % 4 { - 0 => Formation::Marquee, - 1 => Formation::TextmarkConverter, - 2 => Formation::BlockStrata, - _ => Formation::RegisterDump, - } - } else if aspect < 0.65 { - // Tall/narrow — vertical-friendly stuff. - match r % 3 { - 0 => Formation::ParseDump, - 1 => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - _ => Formation::BlockStrata, - } - } else if (aspect - 1.0).abs() < 0.45 && rect.w >= 10 && rect.h >= 6 { - // Square-ish + large enough — save the wow formations for here. - match r % 4 { - 0 => Formation::Raytrace, - 1 => Formation::AttentionMatrix, - 2 => Formation::DensityGrid, - _ => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - } - } else { - // Mid aspect — everything fair game. - match r % 9 { - 0 => Formation::ParseDump, - 1 => Formation::RegisterDump, - 2 => Formation::DensityGrid, - 3 => Formation::TextmarkConverter, - 4 => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - 5 => Formation::BlockStrata, - 6 => Formation::AttentionMatrix, - 7 => Formation::ProbField, - _ => Formation::Marquee, - } - } -} - -struct Zone { - base_rect: Rect, // locked — this is also what's rendered - rect: Rect, // kept for convenience; equals base_rect always - - side: Side, - formation: Formation, - flow_dir: FlowDir, - tap_offset: i32, - pulse: f32, - pulse_rate: f32, - glitch_rate: f32, -} - -fn make_zone( - base: Rect, - side: Side, - formation: Formation, - flow_dir: FlowDir, - tap: i32, - _zone_idx: usize, -) -> Zone { - Zone { - base_rect: base, - rect: base, - side, - formation, - flow_dir, - tap_offset: tap, - pulse: r_f32() * 3.0, - pulse_rate: 0.6 + r_f32() * 1.1, - glitch_rate: if r_f32() < 0.15 { - 0.3 + r_f32() * 0.7 - } else { - 0.0 - }, - } -} - -struct Scene { - w: i32, - h: i32, - stream: Vec, - cursor: f32, - flow_rate: f32, - zones: Vec, - pipes: Vec, - assets: Vec, - adjacency: Vec>, - /// Persistent overlay flow lines (orthogonal + crossed diagonals). - streamers: Vec, - /// Edge-anchored noise injection feeds. - noise_feeds: Vec, - /// Rects that streamers/feeds skip — cube window + UI rects (ATM, - /// AgentSlots, ChessBoard, PayoutButton, TerminalInput). - protected_rects: Vec, - /// Live chess game played by random legal moves — the betting subject. - chess_pos: Chess, - /// Cursor value when last chess move was played. - chess_last_move_at: f32, - /// ATM balance — visible on the panel. - balance: u32, - /// Cursor when balance last ticked (jitters every ~0.3s with ±2% swings). - balance_last_tick: f32, - /// Live text input from the keyboard. - input_buffer: String, - /// Active short-lived image fragments blasted on top of the scene. - glitch_inserts: Vec, - /// Cursor when the last glitch insert was spawned. - last_glitch_spawn: f32, - /// Wandering dither worms — block-density trails that crawl through grids. - dither_flows: Vec, - flipped: bool, - reversed: bool, -} - -/// Bundle of everything a paint function might need to read. -struct PaintCtx<'a> { - stream: &'a [char], - cursor: f32, - zones: &'a [Zone], - adjacency: &'a [Vec], - assets: &'a [ImageAsset], - chess_pos: &'a Chess, - balance: u32, - input_buffer: &'a str, -} - -// ───────────────────────── scene construction ───────────────────────── -fn build_scene(w: i32, h: i32) -> Scene { - seed_from_clock(); - - let mid = w / 2; - // Depth scales with size — no slivers on tiny terminals. - let depth = ((w.min(h * 2)) / 14).clamp(4, 8) as usize; - - let left_spiral = fib_spiral( - Rect { - x: 0, - y: 0, - w: mid, - h, - }, - depth, - true, - ); - let right_spiral = fib_spiral( - Rect { - x: mid, - y: 0, - w: w - mid, - h, - }, - depth, - false, - ); - - let mut zones = Vec::new(); - let mut tap_accum: i64 = 0; - - // LEFT: walk outermost → innermost. Default flow RowMajor. - for rect in &left_spiral { - let side = if r_f32() < 0.08 { Side::Chaos } else { Side::L }; - let flow_dir = if r_f32() < 0.20 { - random_flow() - } else { - FlowDir::RowMajor - }; - let formation = pick_formation(*rect); - let idx = zones.len(); - zones.push(make_zone( - *rect, - side, - formation, - flow_dir, - tap_accum as i32, - idx, - )); - tap_accum += (rect.w * rect.h) as i64; - } - - // RIGHT: walk innermost → outermost so the hose reverses direction, - // creating the mirrored flow. Default flow RowMajorRev. - for rect in right_spiral.iter().rev() { - let side = if r_f32() < 0.08 { Side::Chaos } else { Side::R }; - let flow_dir = if r_f32() < 0.20 { - random_flow() - } else { - FlowDir::RowMajorRev - }; - let formation = pick_formation(*rect); - let idx = zones.len(); - zones.push(make_zone( - *rect, - side, - formation, - flow_dir, - tap_accum as i32, - idx, - )); - tap_accum += (rect.w * rect.h) as i64; - } - - // NESTED CHILDREN — overlay on the 4 biggest zones so the composition has - // "boxes within boxes" at golden-ratio insets. - let mut big_indices: Vec = (0..zones.len()).collect(); - big_indices.sort_by_key(|&i| -(zones[i].base_rect.w * zones[i].base_rect.h)); - for &i in big_indices.iter().take(4) { - let parent = zones[i].base_rect; - if parent.w < 12 || parent.h < 6 { - continue; - } - let child = golden_child(parent); - if child.w < 5 || child.h < 3 { - continue; - } - let formation = pick_formation(child); - let idx = zones.len(); - let mut z = make_zone( - child, - Side::Nested, - formation, - random_flow(), - tap_accum as i32, - idx, - ); - z.glitch_rate = 0.15 + r_f32() * 0.4; - zones.push(z); - tap_accum += (child.w * child.h) as i64; - } - - // Load image assets now so we can assign specific zones to display them. - let assets = load_image_assets(); - - // ─── Size-aware layout ─── compute every UI rect from (w, h) so the - // betting interface scales up to fill any terminal size, always large. - // - // Chess is centered. Square aspect: cell width = 2 × cell height (since - // braille cells are ~2:1 tall). - let chess_h = ((h as f32 * 0.55) as i32).clamp(8, 36); - let mut chess_w = chess_h * 2; - if chess_w > w * 5 / 8 { - chess_w = (w * 5 / 8) & !1; // even - // recompute height to maintain aspect - } - let chess_w = chess_w.clamp(16, 80); - let chess_h = (chess_w / 2).clamp(8, 36); - let ui_chess = Rect { - x: (w - chess_w) / 2, - y: ((h - chess_h) / 2 - 1).max(2), - w: chess_w, - h: chess_h, - }; - - let panel_w = (w / 7).clamp(18, 28); - let panel_h = (h / 9).clamp(4, 6); - - let ui_atm = Rect { - x: w - panel_w - 1, - y: 1, - w: panel_w, - h: panel_h, - }; - let ui_agent_a = Rect { - x: 1, - y: 1, - w: panel_w, - h: panel_h, - }; - let ui_agent_b = Rect { - x: w - panel_w - 1, - y: ui_atm.y + ui_atm.h + 1, - w: panel_w, - h: panel_h, - }; - let ui_payout = Rect { - x: w - panel_w - 1, - y: ui_agent_b.y + ui_agent_b.h + 1, - w: panel_w, - h: panel_h.min(4), - }; - - let term_h = 3_i32; - let term_w = (chess_w + 4).min(w - 4); - let ui_term = Rect { - x: (w - term_w) / 2, - y: h - term_h - 1, - w: term_w, - h: term_h, - }; - - // ─── Dedicated SNAKE image slots ─── carved next to the chess board so - // the vipers stay visible and big. Tall narrow strips on each side. - let img_left_h = (ui_term.y - (ui_agent_a.y + ui_agent_a.h) - 2).max(8); - let ui_viper = Rect { - x: 1, - y: ui_agent_a.y + ui_agent_a.h + 1, - w: panel_w, - h: img_left_h, - }; - let img_right_h = (ui_term.y - (ui_payout.y + ui_payout.h) - 2).max(6); - let ui_vhead = Rect { - x: w - panel_w - 1, - y: ui_payout.y + ui_payout.h + 1, - w: panel_w, - h: img_right_h, - }; - - // Sort the surviving zones by area for asset/formation assignment. - let mut sorted_by_area: Vec = (0..zones.len()) - .filter(|&i| zones[i].side != Side::Nested) - .collect(); - sorted_by_area.sort_by_key(|&i| -(zones[i].base_rect.w * zones[i].base_rect.h)); - - // Seed image panels into the biggest non-Nested survivors. - if !assets.is_empty() { - let mut assigned = 0usize; - let want = assets.len().min(sorted_by_area.len()); - for &i in &sorted_by_area { - let r = zones[i].base_rect; - if r.w < 10 || r.h < 6 { - continue; - } - zones[i].formation = Formation::ImagePanel { - asset: assigned % assets.len(), - }; - assigned += 1; - if assigned >= want { - break; - } - } - } - - let _ui_rects = [ - ui_atm, ui_agent_a, ui_agent_b, ui_chess, ui_payout, ui_term, ui_viper, ui_vhead, - ]; - // NOTE: fib zones are NOT filtered — chaos paints under everything, - // and UI re-paints on top of the chaos in a final pass (see render()). - - // Push UI zones. Each is Side::Nested so they don't participate in pipes. - let mut push_ui = |rect: Rect, formation: Formation, tap: &mut i64| { - zones.push(Zone { - base_rect: rect, - rect, - side: Side::Nested, - formation, - flow_dir: FlowDir::RowMajor, - tap_offset: *tap as i32, - pulse: r_f32() * 3.0, - pulse_rate: 0.8 + r_f32() * 0.5, - glitch_rate: 0.0, - }); - *tap += (rect.w * rect.h) as i64; - }; - push_ui(ui_atm, Formation::Atm, &mut tap_accum); - push_ui( - ui_agent_a, - Formation::AgentSlot { player: 0 }, - &mut tap_accum, - ); - push_ui( - ui_agent_b, - Formation::AgentSlot { player: 1 }, - &mut tap_accum, - ); - push_ui(ui_chess, Formation::ChessBoard, &mut tap_accum); - push_ui(ui_payout, Formation::PayoutButton, &mut tap_accum); - push_ui(ui_term, Formation::TerminalInput, &mut tap_accum); - // SCARY SNAKES — guaranteed visible at decent size. - let viper_idx = if assets.len() > 1 { 1 } else { 0 }; - let vhead_idx = if assets.len() > 2 { 2 } else { viper_idx }; - push_ui( - ui_viper, - Formation::ImagePanel { asset: viper_idx }, - &mut tap_accum, - ); - push_ui( - ui_vhead, - Formation::ImagePanel { asset: vhead_idx }, - &mut tap_accum, - ); - - let stream = build_stream(32_768); - let pipes: Vec = build_pipes(&zones); - - // Adjacency: neighbors = zones a pipe actually connects. - let mut adjacency: Vec> = vec![Vec::new(); zones.len()]; - for p in &pipes { - adjacency[p.from as usize].push(p.to); - adjacency[p.to as usize].push(p.from); - } - - // Persistent overlay streamers — many axes for synapse density. - let streamers = vec![ - // Horizontals at varied rows - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.06) as i32, - speed: 26.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.42) as i32, - speed: 32.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.78) as i32, - speed: 21.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.94) as i32, - speed: 18.0, - direction: -1, - }, - // Verticals on far edges - Streamer { - axis: StreamerAxis::Vertical, - anchor: (w as f32 * 0.04) as i32, - speed: 22.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::Vertical, - anchor: (w as f32 * 0.97) as i32, - speed: 28.0, - direction: 1, - }, - // Diagonals at multiple intercepts - Streamer { - axis: StreamerAxis::DiagPos, - anchor: (w as f32 * 0.02) as i32, - speed: 18.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::DiagPos, - anchor: (w as f32 * 0.45) as i32, - speed: 24.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::DiagNeg, - anchor: (w as f32 * 0.98) as i32, - speed: 17.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::DiagNeg, - anchor: (w as f32 * 0.55) as i32, - speed: 23.0, - direction: -1, - }, - ]; - - // Noise projection feeds — all 4 edges, dense. - let noise_feeds = vec![ - // Top edge - NoiseFeed { - pos: ((w as f32 * 0.10) as i32, 0), - dir: (0, 1), - length: 4, - seed: 0xACE0_BEEF, - speed: 11.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.30) as i32, 0), - dir: (0, 1), - length: 5, - seed: 0xFACE_FADE, - speed: 13.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.50) as i32, 0), - dir: (0, 1), - length: 3, - seed: 0xB001_C0DE, - speed: 15.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.70) as i32, 0), - dir: (0, 1), - length: 4, - seed: 0x1337_C0DE, - speed: 12.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.90) as i32, 0), - dir: (0, 1), - length: 3, - seed: 0xBEEF_F00D, - speed: 16.0, - }, - // Left edge - NoiseFeed { - pos: (0, (h as f32 * 0.30) as i32), - dir: (1, 0), - length: 5, - seed: 0xDEAD_BEEF, - speed: 12.0, - }, - NoiseFeed { - pos: (0, (h as f32 * 0.55) as i32), - dir: (1, 0), - length: 4, - seed: 0xCAFE_F00D, - speed: 14.0, - }, - NoiseFeed { - pos: (0, (h as f32 * 0.78) as i32), - dir: (1, 0), - length: 5, - seed: 0xFEED_BABE, - speed: 10.0, - }, - // Right edge - NoiseFeed { - pos: (w - 1, (h as f32 * 0.30) as i32), - dir: (-1, 0), - length: 5, - seed: 0xDEAD_C0DE, - speed: 14.0, - }, - NoiseFeed { - pos: (w - 1, (h as f32 * 0.55) as i32), - dir: (-1, 0), - length: 4, - seed: 0x4269_4269, - speed: 11.0, - }, - NoiseFeed { - pos: (w - 1, (h as f32 * 0.78) as i32), - dir: (-1, 0), - length: 5, - seed: 0xC001_BEEF, - speed: 15.0, - }, - // Bottom edge - NoiseFeed { - pos: ((w as f32 * 0.20) as i32, h - 1), - dir: (0, -1), - length: 4, - seed: 0xC0DE_F00D, - speed: 12.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.55) as i32, h - 1), - dir: (0, -1), - length: 4, - seed: 0xBEEF_BABE, - speed: 13.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.85) as i32, h - 1), - dir: (0, -1), - length: 5, - seed: 0xFA15_AFE1, - speed: 11.0, - }, - ]; - - // No protected rects — chaos bleeds everywhere. UI re-paints on top. - let protected_rects: Vec = Vec::new(); - - Scene { - w, - h, - stream, - cursor: 0.0, - flow_rate: 48.0, - zones, - pipes, - assets, - adjacency, - streamers, - noise_feeds, - protected_rects, - chess_pos: Chess::default(), - chess_last_move_at: 0.0, - balance: 42_069, - balance_last_tick: 0.0, - input_buffer: String::new(), - glitch_inserts: Vec::new(), - last_glitch_spawn: 0.0, - dither_flows: { - let mut flows = Vec::new(); - for _ in 0..6 { - let speed = 6.0 + r_f32() * 12.0; - let theta = r_f32() * std::f32::consts::TAU; - flows.push(DitherFlow { - pos_x: r_f32() * w as f32, - pos_y: r_f32() * h as f32, - vel_x: theta.cos() * speed, - vel_y: theta.sin() * speed * 0.5, // y velocity halved (terminal aspect) - trail: Vec::new(), - trail_max: 14 + (r_u32() as usize % 18), - }); - } - flows - }, - flipped: false, - reversed: false, - } -} - -// ───────────────────────── simulation ───────────────────────── -fn tick(scene: &mut Scene, dt: f32) { - let sign = if scene.reversed { -1.0 } else { 1.0 }; - scene.cursor += scene.flow_rate * dt * sign; - for z in &mut scene.zones { - z.pulse += dt * z.pulse_rate * sign; - } - // Advance the chess game — one random legal move every ~1.2 seconds (60 cursor units). - if scene.cursor - scene.chess_last_move_at > 60.0 { - scene.chess_last_move_at = scene.cursor; - let moves = scene.chess_pos.legal_moves(); - if moves.is_empty() { - scene.chess_pos = Chess::default(); - } else { - let idx = (r_u32() as usize) % moves.len(); - let mv = moves[idx]; - scene.chess_pos.play_unchecked(mv); - } - } - // Balance tick — every ~14 cursor units (~0.3s) jitter by ±~2%. - if scene.cursor - scene.balance_last_tick > 14.0 { - scene.balance_last_tick = scene.cursor; - let pct = (r_f32() - 0.5) * 0.04; // ±2% - let delta = (scene.balance as f32 * pct) as i64; - let new_bal = (scene.balance as i64 + delta).max(100); - scene.balance = new_bal as u32; - } - - // Tick wandering dither flows. - let w = scene.w; - let h = scene.h; - for flow in &mut scene.dither_flows { - tick_flow(flow, dt * sign, w, h); - } - - // Glitch insertions — random image chunks blasted onto the screen. - // Expire dead ones first. - let cur = scene.cursor; - scene - .glitch_inserts - .retain(|i| (cur - i.spawn_cursor).abs() < i.duration_chars); - // Then maybe spawn a new one. Up to 5 active simultaneously. - if scene.cursor - scene.last_glitch_spawn > 25.0 - && scene.glitch_inserts.len() < 5 - && !scene.assets.is_empty() - { - scene.last_glitch_spawn = scene.cursor; - if r_f32() < 0.85 { - spawn_glitch_insertion(scene); - } - } -} - -fn spawn_glitch_insertion(scene: &mut Scene) { - let asset_idx = (r_u32() as usize) % scene.assets.len(); - let asset = &scene.assets[asset_idx]; - if asset.variants.is_empty() { - return; - } - let variant_idx = (r_u32() as usize) % asset.variants.len(); - let variant = &asset.variants[variant_idx]; - let aw = variant.w as i32; - let ah = variant.h as i32; - - // Random size + position. Chunks range from small to half-screen. - let max_w = (scene.w / 2).max(8); - let max_h = (scene.h * 2 / 3).max(6); - let rw = (8 + (r_u32() as i32 % (max_w - 7).max(1))).min(scene.w - 1); - let rh = (4 + (r_u32() as i32 % (max_h - 3).max(1))).min(scene.h - 1); - let rx = r_u32() as i32 % (scene.w - rw).max(1); - let ry = r_u32() as i32 % (scene.h - rh).max(1); - let rect = Rect { - x: rx, - y: ry, - w: rw, - h: rh, - }; - - // Half the time: full image. Other half: random crop (a strip or chunk). - let crop = if r_f32() < 0.5 { - None - } else { - let cw = (4 + (r_u32() as i32 % (aw - 3).max(1))).min(aw); - let ch = (3 + (r_u32() as i32 % (ah - 2).max(1))).min(ah); - let cx = r_u32() as i32 % (aw - cw).max(1); - let cy = r_u32() as i32 % (ah - ch).max(1); - Some(Rect { - x: cx, - y: cy, - w: cw, - h: ch, - }) - }; - - let duration_chars = 30.0 + r_f32() * 90.0; // ~0.6 to ~2.5 sec @ 48 cps - scene.glitch_inserts.push(GlitchInsertion { - asset_idx, - rect, - crop, - spawn_cursor: scene.cursor, - duration_chars, - variant_idx, - }); -} - -// ───────────────────────── rendering ───────────────────────── -/// Sentinel value meaning "no zone owns this cell yet." -const NO_OWNER: u16 = u16::MAX; - -#[derive(Clone, Copy)] -struct PxCell { - ch: char, - fg: (u8, u8, u8), - intensity: f32, - owner: u16, // zone index that won this cell — used for hard-cutoff masks -} -impl PxCell { - const fn empty() -> Self { - Self { - ch: ' ', - fg: (0, 0, 0), - intensity: 0.0, - owner: NO_OWNER, - } - } -} - -fn put(grid: &mut [Vec], x: i32, y: i32, ch: char, c: (u8, u8, u8), i: f32, owner: u16) { - if y < 0 || x < 0 { - return; - } - let (uy, ux) = (y as usize, x as usize); - if uy >= grid.len() || ux >= grid[0].len() { - return; - } - let cell = &mut grid[uy][ux]; - if i >= cell.intensity { - cell.ch = ch; - cell.fg = c; - cell.intensity = i; - cell.owner = owner; - } -} - -/// Forced paint — used by pipes to bleed across zone boundaries regardless -/// of who owns the cell. Always overwrites. -fn put_force(grid: &mut [Vec], x: i32, y: i32, ch: char, c: (u8, u8, u8), i: f32) { - if y < 0 || x < 0 { - return; - } - let (uy, ux) = (y as usize, x as usize); - if uy >= grid.len() || ux >= grid[0].len() { - return; - } - grid[uy][ux] = PxCell { - ch, - fg: c, - intensity: i, - owner: NO_OWNER, - }; -} - -fn color_for(side: Side) -> (u8, u8, u8) { - // Pure grayscale — signal comes from intensity + char-weight, not hue. - // Side identity survives as small brightness differences at full intensity. - match side { - Side::L | Side::R => (170, 18, 18), // deep matte blood red - Side::Chaos => (255, 90, 90), // hot pink-red pops through - Side::Nested => (230, 40, 40), // bright red, not quite hot - } -} - -#[inline] -fn sample(stream: &[char], idx: i64) -> char { - let n = stream.len() as i64; - stream[idx.rem_euclid(n) as usize] -} - -/// Occasional discrete phase jumps, modulated by zone.pulse. -fn glitch_offset(z: &Zone) -> i64 { - if z.glitch_rate < 0.05 { - return 0; - } - let phase = (z.pulse * z.glitch_rate * 0.6) as i64; - // wrapping_mul by a prime gives chaotic jumps when phase increments. - phase.wrapping_mul(2_039) -} - -// ─────────────── formation paint helpers ─────────────── - -fn ihash(x: i32, y: i32, t: i32) -> u32 { - let mut n = (x as u32) - .wrapping_mul(374_761_393) - .wrapping_add((y as u32).wrapping_mul(668_265_263)) - .wrapping_add((t as u32).wrapping_mul(2_654_435_761)); - n ^= n >> 13; - n = n.wrapping_mul(1_274_126_177); - n ^ (n >> 16) -} - -fn paint_fill(grid: &mut [Vec], zone: &Zone, zone_id: u16, ch: char, i: f32) { - let color = color_for(zone.side); - for ry in 0..zone.rect.h { - for rx in 0..zone.rect.w { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 1. Raytrace window — spinning wireframe sphere. The wow. -fn paint_raytrace(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let w = zone.rect.w as usize; - let h = zone.rect.h as usize; - if w < 4 || h < 3 { - paint_fill(grid, zone, zone_id, '·', 0.20); - return; - } - let mut rt = RtScene::new(); - rt.add_object(Box::new(Sphere::new(Vector3::new(0.0, 0.0, -3.0), 1.1))); - let cam = Camera::new(Vector3::new(0.0, 0.0, 0.0), 4.0, 3.0); - let orient = WireframeRotation { - yaw: zone.pulse * 0.6, - pitch: (zone.pulse * 0.4).sin() * 0.45, - roll: 0.0, - }; - let mode = RenderMode::Wireframe { - step_rad: 15.0_f32.to_radians(), - tol_rad: 0.035, - }; - let buf = render_with_orientation(&rt, &cam, w, h, mode, orient); - - let ramp: &[char] = &[' ', '·', ':', '-', '=', '+', '*', '#', '%', '@']; - for ry in 0..h { - for rx in 0..w { - let v = buf[ry][rx].clamp(0.0, 1.0); - let idx = ((v * (ramp.len() - 1) as f32).round() as usize).min(ramp.len() - 1); - let ch = ramp[idx]; - let i = if v > 0.30 { 0.90 } else { 0.22 }; - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry as i32, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 2. BlockStrata — hard-stepped density bands, no smooth interp. -fn paint_block_strata(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let levels: &[(char, f32)] = &[ - (' ', 0.08), - ('░', 0.38), - ('▒', 0.62), - ('▓', 0.85), - ('█', 1.00), - ]; - let scroll = (zone.pulse * 2.4) as i32; - // Step-function over y: 20-row cycle with custom profile. - for ry in 0..zone.rect.h { - let stripe = (ry + scroll).rem_euclid(20); - let level_idx = match stripe { - 0..=1 => 0, - 2..=4 => 1, - 5..=8 => 2, - 9..=12 => 3, - 13..=15 => 4, - 16..=18 => 3, - _ => 2, - }; - let (ch, i) = levels[level_idx]; - for rx in 0..zone.rect.w { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 3. ParseDump — `xxxx: AB CD EF ...` hex memory dump. -fn paint_parse_dump( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let scroll = base / 4; - - for ry in 0..zh { - let addr = (scroll.wrapping_add(ry as i64) & 0xffff) as u32; - for rx in 0..zw { - let (ch, i) = if rx < 4 { - let nibble = ((addr >> ((3 - rx) * 4)) & 0xf) as usize; - (HEX[nibble], 0.72) - } else if rx == 4 { - (':', 0.55) - } else if rx == 5 { - (' ', 0.10) - } else { - let rel = rx - 6; - let byte_idx = rel / 3; - let pos = rel % 3; - let b = sample(stream, base + (ry as i64) * 9 + byte_idx as i64) as u32; - match pos { - 0 => (HEX[((b >> 4) & 0xf) as usize], 0.92), - 1 => (HEX[(b & 0xf) as usize], 0.92), - _ => (' ', 0.12), - } - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 4. RegisterDump — named registers with hex values. -fn paint_register_dump( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - const NAMES: &[&str] = &[ - "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7", "PC", "SP", "LR", "SR", - ]; - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - - for ry in 0..zh { - let name_cycle = ((base / 8) as usize).wrapping_add(ry as usize) % NAMES.len(); - let name = NAMES[name_cycle]; - let mut row: Vec<(char, f32)> = Vec::with_capacity(zw as usize); - for ch in name.chars() { - row.push((ch, 0.82)); - } - row.push((':', 0.55)); - row.push((' ', 0.10)); - row.push(('0', 0.70)); - row.push(('x', 0.70)); - for i in 0..8 { - let nib = sample(stream, base + (ry as i64) * 5 + i as i64) as u32; - row.push((HEX[(nib & 0xf) as usize], 0.95)); - } - while row.len() < zw as usize { - row.push((' ', 0.10)); - } - for (rx, &(ch, i)) in row.iter().take(zw as usize).enumerate() { - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 5. TextmarkConverter — left side raw stream → `⇒` → right side transformed. -fn paint_textmark( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let mid_y = zh / 2; - let mid_x = zw / 2; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - - let transform = |c: char| -> char { - if c.is_ascii_alphabetic() { - let b = if c.is_ascii_lowercase() { b'a' } else { b'A' }; - let off = ((c as u8) - b + 13) % 26; - (b + off) as char - } else if c.is_ascii_digit() { - let d = (c as u8) - b'0'; - (b'0' + (9 - d)) as char - } else if c.is_ascii() { - HEX[((c as u8) >> 4 & 0x0f) as usize] - } else { - HEX[((c as u32) & 0x0f) as usize] - } - }; - - for ry in 0..zh { - for rx in 0..zw { - if ry == mid_y && rx == mid_x { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - '⇒', - color, - 1.0, - zone_id, - ); - continue; - } - if ry == mid_y { - let (ch, i) = if rx < mid_x { - let c = sample(stream, base + (mid_x - 1 - rx) as i64); - (c, 0.95) - } else { - let c = sample(stream, base + (rx - mid_x - 1) as i64); - (transform(c), 0.95) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } else { - let d = ((ry - mid_y).abs() as f32) / (zh as f32 * 0.5); - let fade = (0.48 - d * 0.32).max(0.12); - let c = sample(stream, base + (ry as i64) * 7 + rx as i64); - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - c, - color, - fade, - zone_id, - ); - } - } - } -} - -/// 6. Cellular1D — elementary CA, seeded from the stream, evolves top-to-bottom. -fn paint_cellular( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - rule: u8, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w as usize; - let zh = zone.rect.h as usize; - if zw < 3 || zh < 2 { - paint_fill(grid, zone, zone_id, '·', 0.20); - return; - } - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let t_shift = (zone.pulse * 2.0) as i64; - - // Seed top row from stream bits. - let mut row = vec![false; zw]; - for rx in 0..zw { - let s = sample(stream, base + rx as i64 + t_shift) as u32; - row[rx] = (s & 1) == 1; - } - // Paint row, then evolve. - for ry in 0..zh { - for rx in 0..zw { - let (ch, i) = if row[rx] { ('█', 0.93) } else { ('·', 0.18) }; - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry as i32, - ch, - color, - i, - zone_id, - ); - } - if ry + 1 >= zh { - break; - } - let prev = row.clone(); - for rx in 0..zw { - let l = prev[(rx + zw - 1) % zw]; - let c = prev[rx]; - let r = prev[(rx + 1) % zw]; - let pat = ((l as u8) << 2) | ((c as u8) << 1) | (r as u8); - row[rx] = ((rule >> pat) & 1) == 1; - } - } -} - -/// 8. Marquee — scrolling stream text with bright middle band. -fn paint_marquee( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let base: i64 = (cursor as i64) - (zone.tap_offset as i64) + glitch_offset(zone); - let zw = zone.rect.w; - let zh = zone.rect.h; - let mid_y = zh / 2; - for ry in 0..zh { - for rx in 0..zw { - let ci = cell_index(rx, ry, zw, zh, zone.flow_dir); - let ch = sample(stream, base + ci); - let i = if ry == mid_y { - 1.0 - } else { - let d = ((ry - mid_y).abs() as f32) / (zh as f32 * 0.5); - (0.78 - d * 0.48).max(0.30) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 9. DensityGrid — 2-cell block mosaic at stream-byte density. -fn paint_density_grid( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let levels: &[(char, f32)] = &[ - (' ', 0.08), - ('░', 0.35), - ('▒', 0.58), - ('▓', 0.82), - ('█', 1.00), - ]; - let block_w: i32 = 2; - let blocks_per_row = (zone.rect.w + block_w - 1) / block_w; - for ry in 0..zone.rect.h { - for bx in 0..blocks_per_row { - let rx0 = bx * block_w; - let idx = (ry as i64) * (blocks_per_row as i64) + bx as i64; - let s = sample(stream, base + idx) as u32; - let density = ((s & 0xff) as usize * levels.len()) / 256; - let density = density.min(levels.len() - 1); - let (ch, i) = levels[density]; - for k in 0..block_w { - let rx = rx0 + k; - if rx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } - } -} - -/// 10. AttentionMatrix — sparse transformer-attention pattern: diagonal band, -/// a few sink columns, rare hotspots. Everything else mostly dark. -fn paint_attention(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let t = zone.pulse; - - // A handful of "sink" columns (attention sinks) that migrate slowly. - let n_sinks = (1 + (zw / 14)).max(2); - let mut sinks: Vec = Vec::with_capacity(n_sinks as usize); - for i in 0..n_sinks { - let phase = (t * 0.2 + i as f32 * 0.7).sin(); - let pos = (((phase + 1.0) * 0.5) * (zw as f32 - 2.0)) as i32 + 1; - sinks.push(pos.clamp(0, zw - 1)); - } - - for ry in 0..zh { - for rx in 0..zw { - let mut score: f32 = 0.12; - - // Diagonal band — attending to self/near tokens. - let diag_x = (ry as f32 / zh.max(1) as f32) * (zw as f32); - let d = (diag_x - rx as f32).abs(); - if d < 2.0 { - score = score.max(0.88 - d * 0.25); - } - - // Sink columns — always some attention. - for &s in &sinks { - let cd = (rx - s).abs(); - if cd == 0 { - score = score.max(0.78); - } else if cd == 1 { - score = score.max(0.42); - } - } - - // Rare random hotspots that shimmer with time. - let h = ihash(rx, ry, (t * 2.0) as i32); - if (h & 0xff) < 4 { - score = score.max(0.95); - } - - let (ch, i) = if score > 0.85 { - ('█', 1.0) - } else if score > 0.60 { - ('▓', 0.80) - } else if score > 0.38 { - ('▒', 0.55) - } else if score > 0.18 { - ('░', 0.32) - } else { - ('.', 0.14) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 11. ProbField — top-K token distribution, **conditioned on neighbors**. -/// -/// Each row's probability is derived by sampling the hose at one of this -/// zone's neighbors' current windows. If a zone has no neighbors (isolated, -/// rare), it falls back to its own tap. The ordering by mass is real: the -/// distribution collapses onto a top candidate each frame, with the runners-up -/// visibly competing below it. As the hose advances, the neighbors' views -/// shift, and this zone's entire distribution reshuffles in response — a -/// picture of attention doing what attention does. -fn paint_prob_field(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &PaintCtx) { - const CANDIDATES: &[&str] = &[ - "the", "and", "to", "of", "is", "a", "in", "that", "it", "for", "fn", "::", "0x", "->", - "=>", "if", "fold", "self", "void", "phi", "sigma", "delta", "ROUTINE", "ACK", "MOV", - "yield", "loop", "ok", "recur", "echo", "bind", "map", "tau", "λ", "∇", - ]; - let color = color_for(zone.side); - let zw = zone.rect.w as usize; - let zh = zone.rect.h as usize; - if zw < 14 || zh < 2 { - paint_fill(grid, zone, zone_id, '·', 0.25); - return; - } - - // Collect this zone's neighbors' tap offsets. Fall back to own tap if - // isolated so the formation still reads coherently. - let neighbors = &ctx.adjacency[zone_id as usize]; - let tap_pool: Vec = if neighbors.is_empty() { - vec![zone.tap_offset] - } else { - neighbors - .iter() - .map(|&j| ctx.zones[j as usize].tap_offset) - .collect() - }; - - let top_k = zh.min(16); - let bar_width = zw.saturating_sub(13).max(4); - - // Each row samples from ONE neighbor's current window — the row's weight - // is what that neighbor is "focusing on" right now. Skew-cubed so one or - // two candidates dominate (real LM distributions have heavy peaks). - let mut probs: Vec<(f32, &str)> = Vec::with_capacity(top_k); - let mut sum = 0.0_f32; - for i in 0..top_k { - let tap = tap_pool[i % tap_pool.len()]; - let neighbor_window_offset = (i as i64) * 23 + (ctx.cursor as i64 / 3); - let s = sample( - ctx.stream, - (ctx.cursor as i64) - (tap as i64) + neighbor_window_offset, - ) as u32; - let raw = 0.01 + ((s & 0xff) as f32) / 255.0; - let weight = raw.powi(3); - sum += weight; - let name = CANDIDATES[(s as usize >> 4) % CANDIDATES.len()]; - probs.push((weight, name)); - } - for p in &mut probs { - p.0 /= sum.max(1e-6); - } - probs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); - - paint_fill(grid, zone, zone_id, ' ', 0.08); - - for (row, (p, name)) in probs.iter().enumerate().take(zh) { - let ry = row as i32; - let fill = ((*p * bar_width as f32).round() as usize).min(bar_width); - for rx in 0..(bar_width as i32) { - let (ch, i) = if (rx as usize) < fill { - ('█', (0.55 + p * 0.45).min(1.0)) - } else { - ('░', 0.20) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - let p_str = format!(" {:.2}", p.min(0.99)); - let mut cx = bar_width as i32 + 1; - for ch in p_str.chars() { - if cx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + cx, - zone.rect.y + ry, - ch, - color, - 0.82, - zone_id, - ); - cx += 1; - } - cx += 1; - for ch in name.chars() { - if cx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + cx, - zone.rect.y + ry, - ch, - color, - 0.95, - zone_id, - ); - cx += 1; - } - } -} - -/// 12. ImagePanel — streams a pre-rendered braille image into the zone with -/// glitching effects. Source: dotmax's full ImageRenderer pipeline -/// (Floyd-Steinberg → Otsu → braille mapping). Each frame, a few rows get -/// scanline-torn, a sprinkle of cells get block-char corrupted, and bursts -/// of stream chars bleed through as noise. -// ─────────────── abstract dither-phase system ─────────────── -// -// Instead of a smooth per-cell wave, the composition is in one of two -// macro-phases at any moment: -// -// • Stable — ~11 real-seconds of a SINGLE dither variant everywhere -// • Sweep — ~1.6 seconds where a geometric sweep-front cuts across -// every image panel, carving the old variant away and -// crystallizing the new one behind it. A bright heavy-block -// highlight marks the sweep front at all times. -// -// All image panels share the same phase/timing — synchronized, deliberate, -// momentous. The sweep direction rotates per cycle (horizontal, vertical, -// diagonal, anti-diagonal, radial). - -#[derive(Clone, Copy)] -enum SweepKind { - Horizontal, - Vertical, - Diagonal, - AntiDiag, - Radial, -} - -#[derive(Clone, Copy)] -enum DitherPhase { - Stable { - idx: usize, - }, - Sweep { - from: usize, - to: usize, - t: f32, - kind: SweepKind, - }, -} - -const DITHER_NAMES: &[&str] = &["NONE", "FLOYD", "BAYER", "ATKIN"]; - -/// Convert the global cursor into a dither phase, with a per-zone offset -/// (in seconds) so different image panels run on their own clocks. -/// Asynchronous — each panel transitions when ITS clock says so. -fn dither_phase(cursor: f32, n_variants: usize, offset_secs: f32) -> DitherPhase { - const PERIOD: f32 = 11.0; - const TRANSIT: f32 = 1.6; - let cycle = PERIOD + TRANSIT; - let secs = (cursor / 48.0) + offset_secs; - let cycle_num = secs.div_euclid(cycle) as i64; - let in_cycle = secs.rem_euclid(cycle); - - let kind = match (cycle_num.rem_euclid(5)) as usize { - 0 => SweepKind::Horizontal, - 1 => SweepKind::Vertical, - 2 => SweepKind::Diagonal, - 3 => SweepKind::AntiDiag, - _ => SweepKind::Radial, - }; - - if in_cycle < PERIOD { - DitherPhase::Stable { - idx: (cycle_num.rem_euclid(n_variants as i64)) as usize, - } - } else { - let raw = ((in_cycle - PERIOD) / TRANSIT).clamp(0.0, 1.0); - // Smoothstep — dramatic ease-in/out rather than linear. - let t = raw * raw * (3.0 - 2.0 * raw); - let from = cycle_num.rem_euclid(n_variants as i64) as usize; - let to = (cycle_num + 1).rem_euclid(n_variants as i64) as usize; - DitherPhase::Sweep { from, to, t, kind } - } -} - -/// Sacred glyph cycled per sweep kind — the symbol that marks the moment of -/// transition. Each geometric sweep wears its own sign. -#[inline] -fn sweep_front_glyph(kind: SweepKind) -> char { - match kind { - SweepKind::Horizontal => '✦', // four-pointed star - SweepKind::Vertical => '✧', // outlined star - SweepKind::Diagonal => '◉', // circled dot - SweepKind::AntiDiag => '☸', // wheel of dharma - SweepKind::Radial => '⚘', // flower - } -} - -/// Progress at cell (rx, ry) along the sweep direction, ∈ [0, 1]. -#[inline] -fn sweep_progress(rx: i32, ry: i32, zw: i32, zh: i32, kind: SweepKind) -> f32 { - let zwf = zw.max(1) as f32; - let zhf = zh.max(1) as f32; - match kind { - SweepKind::Horizontal => rx as f32 / zwf, - SweepKind::Vertical => ry as f32 / zhf, - SweepKind::Diagonal => (rx as f32 + (ry as f32) * 2.0) / (zwf + zhf * 2.0), - SweepKind::AntiDiag => ((zwf - rx as f32 - 1.0) + (ry as f32) * 2.0) / (zwf + zhf * 2.0), - SweepKind::Radial => { - let cx = zwf * 0.5; - let cy = zhf * 0.5; - let dx = rx as f32 - cx; - let dy = (ry as f32 - cy) * 2.0; - let d = (dx * dx + dy * dy).sqrt(); - let max_d = ((cx * cx) + (cy * 2.0).powi(2)).sqrt().max(0.01); - d / max_d - } - } -} - -fn paint_image_panel( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - asset_idx: usize, - ctx: &PaintCtx, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - if ctx.assets.is_empty() || asset_idx >= ctx.assets.len() { - paint_fill(grid, zone, zone_id, '·', 0.3); - return; - } - let asset = &ctx.assets[asset_idx]; - if asset.variants.is_empty() { - paint_fill(grid, zone, zone_id, '·', 0.3); - return; - } - let n_variants = asset.variants.len(); - // Per-zone offset (seconds) — each panel runs on its own dither clock. - // tap_offset is a stable per-zone integer; modulate it to seconds. - let zone_offset = (zone.tap_offset as f32 * 0.0019) + zone.pulse * 0.7; - let phase = dither_phase(ctx.cursor, n_variants, zone_offset); - - // Glitch modulation from pulse (still there for texture, not dither). - let pulse_i = (zone.pulse * 3.7) as i32; - let tear_rows: [i32; 3] = [ - ((zone.pulse * 4.2).sin() * zh as f32) as i32 % zh.max(1), - ((zone.pulse * 1.9 + 1.3).sin() * zh as f32) as i32 % zh.max(1), - ((zone.pulse * 2.6 + 3.1).sin() * zh as f32) as i32 % zh.max(1), - ]; - let tear_amts: [i32; 3] = [ - ((zone.pulse * 5.3).sin() * 6.0) as i32, - ((zone.pulse * 3.8 + 0.7).sin() * 4.0) as i32, - ((zone.pulse * 2.1 + 2.4).sin() * 8.0) as i32, - ]; - - let base_stream: i64 = (ctx.cursor as i64) - (zone.tap_offset as i64); - - // Width of the bright sweep front as a fraction of the total sweep distance. - const FRONT_BAND: f32 = 0.035; - - // Native-resolution scrolling: image is sampled 1:1 from braille cells - // and wraps modularly. The frame stays fixed; the image scrolls inside - // it. No warping — aspect ratio preserved. - let scroll_x = (zone.pulse * 1.7) as i32; - let scroll_y = (zone.pulse * 0.9) as i32; - - for ry in 0..zh { - let mut tear_dx = 0_i32; - for k in 0..3 { - if ry == tear_rows[k] { - tear_dx = tear_amts[k]; - } - } - - for rx in 0..zw { - // Decide which variant owns this cell, + whether this cell is - // currently ON the sweep front (gets a bright highlight). - let (v_idx, on_front, front_kind) = match phase { - DitherPhase::Stable { idx } => (idx, false, SweepKind::Horizontal), - DitherPhase::Sweep { from, to, t, kind } => { - let p = sweep_progress(rx, ry, zw, zh, kind); - let front = (p - t).abs() < FRONT_BAND; - let idx = if p < t { to } else { from }; - (idx, front, kind) - } - }; - let variant = &asset.variants[v_idx.min(n_variants - 1)]; - - let vw = variant.w as i32; - let vh = variant.h as i32; - let srx = (rx + tear_dx + scroll_x).rem_euclid(vw.max(1)) as usize; - let sry = (ry + scroll_y).rem_euclid(vh.max(1)) as usize; - let img_ch = variant - .cells - .get(sry) - .and_then(|row| row.get(srx)) - .copied() - .unwrap_or(' '); - - let h = ihash(rx, ry, pulse_i); - let (ch, intensity, fg_override) = if on_front { - // Sweep front — sacred glyph marks the moment of transition. - (sweep_front_glyph(front_kind), 1.0, Some((255, 50, 50))) - } else if h & 0x7f == 0 { - let blocks: &[char] = &['█', '▓', '▒', '░']; - (blocks[(h as usize >> 7) % blocks.len()], 1.0, None) - } else if h & 0x3f == 0 { - let sc = sample(ctx.stream, base_stream + (ry as i64) * 7 + rx as i64); - (sc, 0.85, None) - } else if img_ch == '\u{2800}' { - (' ', 0.08, None) - } else { - (img_ch, 0.92, None) - }; - let fg = fg_override.unwrap_or(color); - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - fg, - intensity, - zone_id, - ); - } - } - - // Top-left: asset name. - let label = asset.name; - for (i, ch) in label.chars().enumerate() { - let lx = zone.rect.x + 1 + i as i32; - if i as i32 + 1 < zw { - put(grid, lx, zone.rect.y, ch, (255, 50, 50), 1.0, zone_id); - } - } - // Bottom-right: dither phase tag — calculated readout of state. - let tag: String = match phase { - DitherPhase::Stable { idx } => { - format!("[{}]", DITHER_NAMES[idx.min(DITHER_NAMES.len() - 1)]) - } - DitherPhase::Sweep { from, to, .. } => format!( - "{}→{}", - DITHER_NAMES[from.min(DITHER_NAMES.len() - 1)], - DITHER_NAMES[to.min(DITHER_NAMES.len() - 1)], - ), - }; - let tag_y = zone.rect.y + zh - 1; - let tag_x_start = zone.rect.x + zw - (tag.chars().count() as i32) - 1; - for (i, ch) in tag.chars().enumerate() { - let lx = tag_x_start + i as i32; - if lx >= zone.rect.x && lx < zone.rect.x + zw { - put(grid, lx, tag_y, ch, (255, 50, 50), 1.0, zone_id); - } - } -} - -/// 13. RaytraceCube — wireframe cube on BLACK background. Built from 8 -/// corners + 12 edges, rotated via raytracer's yaw/pitch helper. Lines -/// rasterized with Bresenham. Zero mask, zero pipes — pure geometry in -/// the middle of the chaos. -fn paint_raytrace_cube(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let zw = zone.rect.w; - let zh = zone.rect.h; - if zw < 6 || zh < 4 { - paint_fill(grid, zone, zone_id, ' ', 0.05); - return; - } - - // Fill zone with true black bg — claim ownership at low positive intensity. - for ry in 0..zh { - for rx in 0..zw { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ' ', - (0, 0, 0), - 0.02, - zone_id, - ); - } - } - - // 8 corners of a unit cube centered at origin. - let corners = [ - Vector3::new(-1.0, -1.0, -1.0), - Vector3::new(1.0, -1.0, -1.0), - Vector3::new(1.0, 1.0, -1.0), - Vector3::new(-1.0, 1.0, -1.0), - Vector3::new(-1.0, -1.0, 1.0), - Vector3::new(1.0, -1.0, 1.0), - Vector3::new(1.0, 1.0, 1.0), - Vector3::new(-1.0, 1.0, 1.0), - ]; - let edges: [(usize, usize); 12] = [ - (0, 1), - (1, 2), - (2, 3), - (3, 0), // back face - (4, 5), - (5, 6), - (6, 7), - (7, 4), // front face - (0, 4), - (1, 5), - (2, 6), - (3, 7), // connecting edges - ]; - - let yaw = zone.pulse * 0.55; - let pitch = (zone.pulse * 0.37).sin() * 0.45; - let roll = (zone.pulse * 0.22).cos() * 0.25; - - // Project each rotated corner to zone cell coords. - let zwf = zw as f32; - let zhf = zh as f32; - let cam_z = 3.2_f32; - let viewport_w = 2.8_f32; - let viewport_h = 2.8_f32; - - let projected: [Option<(i32, i32)>; 8] = { - let mut out = [None; 8]; - for (i, &c) in corners.iter().enumerate() { - let r = rotate_vec_yaw_pitch_roll(c, yaw, pitch, roll); - let z = r.z - cam_z; - if z >= -0.01 { - continue; - } // behind camera - let px = r.x * (-1.0 / z) / (viewport_w * 0.5); - let py = r.y * (-1.0 / z) / (viewport_h * 0.5); - let cx = (px + 1.0) * 0.5 * zwf; - let cy = (1.0 - (py + 1.0) * 0.5) * zhf * 2.0; // aspect correction - let cy = cy * 0.5; - out[i] = Some((cx.round() as i32, cy.round() as i32)); - } - out - }; - - // Draw edges via Bresenham. Back edges (those with any corner having - // a more negative rotated z) rendered with thinner chars — cheap hidden-line hint. - for &(a, b) in &edges { - if let (Some((x0, y0)), Some((x1, y1))) = (projected[a], projected[b]) { - let dx = (x1 - x0).abs(); - let dy = -(y1 - y0).abs(); - let sx = if x0 < x1 { 1 } else { -1 }; - let sy = if y0 < y1 { 1 } else { -1 }; - let mut err = dx + dy; - let (mut x, mut y) = (x0, y0); - loop { - if x >= 0 && x < zw && y >= 0 && y < zh { - put( - grid, - zone.rect.x + x, - zone.rect.y + y, - '█', - (220, 35, 35), - 1.0, - zone_id, - ); - } - if x == x1 && y == y1 { - break; - } - let e2 = 2 * err; - if e2 >= dy { - err += dy; - x += sx; - } - if e2 <= dx { - err += dx; - y += sy; - } - } - } - } -} - -// ─────────────── betting UI formations ─────────────── - -fn paint_box_border(grid: &mut [Vec], zone: &Zone, zone_id: u16, intensity: f32) { - let zw = zone.rect.w; - let zh = zone.rect.h; - if zw < 2 || zh < 2 { - return; - } - for x in 1..zw - 1 { - put( - grid, - zone.rect.x + x, - zone.rect.y, - '─', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + x, - zone.rect.y + zh - 1, - '─', - UI_WHITE, - intensity, - zone_id, - ); - } - for y in 1..zh - 1 { - put( - grid, - zone.rect.x, - zone.rect.y + y, - '│', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + y, - '│', - UI_WHITE, - intensity, - zone_id, - ); - } - put( - grid, - zone.rect.x, - zone.rect.y, - '┌', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y, - '┐', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x, - zone.rect.y + zh - 1, - '└', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + zh - 1, - '┘', - UI_WHITE, - intensity, - zone_id, - ); -} - -fn put_str( - grid: &mut [Vec], - x: i32, - y: i32, - s: &str, - color: (u8, u8, u8), - i: f32, - oid: u16, - max_x: i32, -) { - let mut cx = x; - for ch in s.chars() { - if cx >= max_x { - break; - } - put(grid, cx, y, ch, color, i, oid); - cx += 1; - } -} - -/// Atm — top-corner balance display. -fn paint_atm(grid: &mut [Vec], zone: &Zone, zone_id: u16, balance: u32, cursor: f32) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 1.0); - let ix = zone.rect.x + 2; - let max_x = zone.rect.x + zone.rect.w - 1; - put_str( - grid, - ix, - zone.rect.y + 1, - "ATM ::", - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - let bal = format!("$ {:08}", balance); - put_str( - grid, - ix, - zone.rect.y + 2, - &bal, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - // Tiny cursor indicator - let blink = ((cursor * 0.5) as i32) % 2 == 0; - if blink && zone.rect.h >= 4 { - put_str( - grid, - ix, - zone.rect.y + 3, - "● ONLINE", - (255, 90, 90), - 1.0, - zone_id, - max_x, - ); - } else if zone.rect.h >= 4 { - put_str( - grid, - ix, - zone.rect.y + 3, - "○ ONLINE", - (200, 60, 60), - 0.9, - zone_id, - max_x, - ); - } -} - -/// AgentSlot — labeled "ENTER AGENT" panel for a single player. -fn paint_agent_slot( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - player: u8, - scene_input: &str, -) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 0.9); - let ix = zone.rect.x + 2; - let max_x = zone.rect.x + zone.rect.w - 1; - let label = if player == 0 { "AGENT P1" } else { "AGENT P2" }; - put_str( - grid, - ix, - zone.rect.y + 1, - label, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - let prompt = "[ENTER AGENT >]"; - put_str( - grid, - ix, - zone.rect.y + 2, - prompt, - (255, 110, 110), - 1.0, - zone_id, - max_x, - ); - // Show typed input under P1 only (one buffer for the demo). - if player == 0 && !scene_input.is_empty() && zone.rect.h >= 4 { - let truncated: String = scene_input - .chars() - .take((zone.rect.w - 4) as usize) - .collect(); - put_str( - grid, - ix, - zone.rect.y + 3, - &truncated, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - } -} - -/// Live chess board — converts shakmaty position to braille via dotmax::chess. -/// During global dither sweeps, a sacred sweep-front cuts across the board so -/// the chess "dithers into" the center in lockstep with the image panels. -fn paint_chess_board( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - pos: &Chess, - cursor: f32, -) { - paint_fill(grid, zone, zone_id, ' ', 0.04); - let opts = RenderOptions { - target_width: Some(zone.rect.w as usize), - target_height: Some(zone.rect.h as usize), - ..Default::default() - }; - if let Ok(braille_grid) = render_position_with_options(pos, &opts) { - let (gw, gh) = braille_grid.dimensions(); - // Determine sweep state for the dither overlay. - let phase = dither_phase(cursor, 4, 0.0); - for ry in 0..zone.rect.h.min(gh as i32) { - for rx in 0..zone.rect.w.min(gw as i32) { - let ch = braille_grid.get_char(rx as usize, ry as usize); - let (color, intensity) = if ch == '\u{2800}' || ch == ' ' { - ((50, 5, 5), 0.30) - } else { - (UI_WHITE, 1.0) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - intensity, - zone_id, - ); - } - } - // Chess "dithers in" — sweep front overlay during transitions. - if let DitherPhase::Sweep { t, kind, .. } = phase { - let zw = zone.rect.w; - let zh = zone.rect.h; - for ry in 0..zh { - for rx in 0..zw { - let p = sweep_progress(rx, ry, zw, zh, kind); - if (p - t).abs() < 0.04 { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - sweep_front_glyph(kind), - UI_WHITE, - 1.0, - zone_id, - ); - } - } - } - } - } -} - -/// Big "[ CASH OUT ]" payout button. Solid block frame, white centered text. -fn paint_payout_button(grid: &mut [Vec], zone: &Zone, zone_id: u16, cursor: f32) { - let zw = zone.rect.w; - let zh = zone.rect.h; - paint_fill(grid, zone, zone_id, ' ', 0.0); - // Solid block border - for x in 0..zw { - put( - grid, - zone.rect.x + x, - zone.rect.y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - put( - grid, - zone.rect.x + x, - zone.rect.y + zh - 1, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - } - for y in 0..zh { - put( - grid, - zone.rect.x, - zone.rect.y + y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - } - // Pulsing label - let text = "[ CASH OUT ]"; - let len = text.chars().count() as i32; - let mid_y = zone.rect.y + zh / 2; - let mid_x = zone.rect.x + (zw - len) / 2; - let pulse = ((cursor * 0.05).sin() + 1.0) * 0.5; // 0..1 - let color = ( - 255, - (60.0 + pulse * 60.0) as u8, - (60.0 + pulse * 60.0) as u8, - ); - put_str( - grid, - mid_x, - mid_y, - text, - color, - 1.0, - zone_id, - zone.rect.x + zw - 1, - ); -} - -/// Live terminal input — prompt + buffer + blinking cursor. -fn paint_terminal_input( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - buffer: &str, - cursor: f32, -) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 0.9); - let ix = zone.rect.x + 2; - let mid_y = zone.rect.y + zone.rect.h / 2; - let max_x = zone.rect.x + zone.rect.w - 1; - let prompt = "INPUT> "; - put_str(grid, ix, mid_y, prompt, UI_WHITE, 1.0, zone_id, max_x); - let mut bx = ix + prompt.chars().count() as i32; - for ch in buffer.chars() { - if bx >= max_x - 1 { - break; - } - put(grid, bx, mid_y, ch, UI_WHITE, 1.0, zone_id); - bx += 1; - } - // Blinking cursor block - let blink = ((cursor / 24.0) as i32) % 2 == 0; - if blink && bx < max_x { - put(grid, bx, mid_y, '█', UI_WHITE, 1.0, zone_id); - } -} - -// ─────────────── formation dispatch ─────────────── - -fn paint_formation(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &PaintCtx) { - match zone.formation { - Formation::Raytrace => paint_raytrace(grid, zone, zone_id), - Formation::BlockStrata => paint_block_strata(grid, zone, zone_id), - Formation::ParseDump => paint_parse_dump(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::RegisterDump => paint_register_dump(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::TextmarkConverter => paint_textmark(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::Cellular1D { rule } => { - paint_cellular(grid, zone, zone_id, rule, ctx.stream, ctx.cursor) - } - Formation::Marquee => paint_marquee(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::DensityGrid => paint_density_grid(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::AttentionMatrix => paint_attention(grid, zone, zone_id), - Formation::ProbField => paint_prob_field(grid, zone, zone_id, ctx), - Formation::ImagePanel { asset } => paint_image_panel(grid, zone, zone_id, asset, ctx), - Formation::RaytraceCube => paint_raytrace_cube(grid, zone, zone_id), - Formation::Atm => paint_atm(grid, zone, zone_id, ctx.balance, ctx.cursor), - Formation::AgentSlot { player } => { - paint_agent_slot(grid, zone, zone_id, player, ctx.input_buffer) - } - Formation::ChessBoard => paint_chess_board(grid, zone, zone_id, ctx.chess_pos, ctx.cursor), - Formation::PayoutButton => paint_payout_button(grid, zone, zone_id, ctx.cursor), - Formation::TerminalInput => { - paint_terminal_input(grid, zone, zone_id, ctx.input_buffer, ctx.cursor) - } - } -} - -/// Compression operator applied by a pipe as chars flow through it. -#[derive(Clone, Copy)] -enum Transform { - /// XOR each byte with the key. - Xor(u8), - /// ROT-N on ASCII letters (N = signed shift). - Rot(i8), - /// Encode low nibble as a hex digit. - HexEncode, - /// Reverse byte bits. - BitRev, - /// Keep every other char, drop the rest to '|'. - Stripe, -} - -fn pick_transform() -> Transform { - match r_u32() % 5 { - 0 => Transform::Xor(0x33 + ((r_u32() as u8) & 0x7F)), - 1 => Transform::Rot((1 + (r_u32() % 25)) as i8), - 2 => Transform::HexEncode, - 3 => Transform::BitRev, - _ => Transform::Stripe, - } -} - -fn apply_transform(c: char, t: Transform) -> char { - match t { - Transform::Xor(k) => { - if c.is_ascii() { - let b = (c as u8) ^ k; - if (b as char).is_ascii_graphic() { - b as char - } else { - HEX[(b & 0x0f) as usize] - } - } else { - HEX[((c as u32) & 0x0f) as usize] - } - } - Transform::Rot(n) => { - if c.is_ascii_alphabetic() { - let base = if c.is_ascii_uppercase() { b'A' } else { b'a' }; - let shifted = (((c as u8 - base) as i16 + n as i16).rem_euclid(26)) as u8; - (base + shifted) as char - } else { - c - } - } - Transform::HexEncode => HEX[((c as u32) & 0x0f) as usize], - Transform::BitRev => { - if c.is_ascii() { - let mut b = c as u8; - b = (b >> 4) | (b << 4); - b = ((b >> 2) & 0x33) | ((b << 2) & 0xcc); - b = ((b >> 1) & 0x55) | ((b << 1) & 0xaa); - if (b as char).is_ascii_graphic() { - b as char - } else { - HEX[(b & 0x0f) as usize] - } - } else { - HEX[((c as u32) & 0x0f) as usize] - } - } - Transform::Stripe => { - if (c as u32) & 1 == 0 { - '|' - } else { - c - } - } - } -} - -/// Operator zone symbols — drawn in the middle of every pipe to identify -/// the compression happening inline. Variable-length: 2 or 3 chars. -fn transform_symbols(t: Transform) -> [char; 3] { - match t { - Transform::Xor(k) => ['⊕', HEX[((k >> 4) & 0xf) as usize], HEX[(k & 0xf) as usize]], - Transform::Rot(n) => { - let mag = (n.unsigned_abs() as usize) % 26; - ['↻', HEX[(mag / 16) as usize], HEX[(mag % 16) as usize]] - } - Transform::HexEncode => ['#', '1', '6'], - Transform::BitRev => ['⊥', '↔', '⊥'], - Transform::Stripe => ['▮', '|', '▮'], - } -} - -/// Pipe: an active conduit between two zones. Cells run from inside the source -/// zone, across the shared border, into the destination zone. Along the pipe, -/// chars pass through three zones of painting: -/// [INPUT: raw source chars] → [OPERATOR: transform symbol] → [OUTPUT: transformed] -/// The whole composition becomes a compression machine — each pipe a stage. -struct Pipe { - from: u16, - to: u16, - cells: Vec<(i32, i32)>, // ordered source-end → dest-end - transform: Transform, - step: i64, // stream-chars between consecutive pipe cells (1..=4) -} - -/// Try to build a pipe between two adjacent zones. Returns None if they -/// aren't adjacent or the shared edge is too short to carry a useful pipe. -fn try_build_pipe(zones: &[Zone], i: usize, j: usize) -> Option { - let a = zones[i].base_rect; - let b = zones[j].base_rect; - let min_edge = 4; - let len_each = 5; // cells extending into each zone from the shared border - - // Helper to finish the Pipe once `cells` are built - let mk = |from: u16, to: u16, cells: Vec<(i32, i32)>| -> Pipe { - Pipe { - from, - to, - cells, - transform: pick_transform(), - step: 1 + (r_u32() % 4) as i64, // per-pipe flow granularity - } - }; - - // A-right touches B-left (flow rightwards: from A into B) - if a.x + a.w == b.x { - let y0 = a.y.max(b.y); - let y1 = (a.y + a.h).min(b.y + b.h); - if y1 - y0 < min_edge { - return None; - } - let y = y0 + (y1 - y0) / 2; - let l_a = len_each.min(a.w - 1).max(2); - let l_b = len_each.min(b.w - 1).max(2); - let cells: Vec<(i32, i32)> = ((a.x + a.w - l_a)..(b.x + l_b)).map(|x| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(i as u16, j as u16, cells)); - } - } - // B-right touches A-left (flow rightwards: from B into A) - if b.x + b.w == a.x { - let y0 = a.y.max(b.y); - let y1 = (a.y + a.h).min(b.y + b.h); - if y1 - y0 < min_edge { - return None; - } - let y = y0 + (y1 - y0) / 2; - let l_a = len_each.min(a.w - 1).max(2); - let l_b = len_each.min(b.w - 1).max(2); - let cells: Vec<(i32, i32)> = ((b.x + b.w - l_b)..(a.x + l_a)).map(|x| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(j as u16, i as u16, cells)); - } - } - // A-bottom touches B-top (flow downwards: from A into B) - if a.y + a.h == b.y { - let x0 = a.x.max(b.x); - let x1 = (a.x + a.w).min(b.x + b.w); - if x1 - x0 < min_edge { - return None; - } - let x = x0 + (x1 - x0) / 2; - let l_a = 4.min(a.h - 1).max(2); - let l_b = 4.min(b.h - 1).max(2); - let cells: Vec<(i32, i32)> = ((a.y + a.h - l_a)..(b.y + l_b)).map(|y| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(i as u16, j as u16, cells)); - } - } - // B-bottom touches A-top (flow downwards: from B into A) - if b.y + b.h == a.y { - let x0 = a.x.max(b.x); - let x1 = (a.x + a.w).min(b.x + b.w); - if x1 - x0 < min_edge { - return None; - } - let x = x0 + (x1 - x0) / 2; - let l_a = 4.min(a.h - 1).max(2); - let l_b = 4.min(b.h - 1).max(2); - let cells: Vec<(i32, i32)> = ((b.y + b.h - l_b)..(a.y + l_a)).map(|y| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(j as u16, i as u16, cells)); - } - } - None -} - -fn build_pipes(zones: &[Zone]) -> Vec { - // Build a pipe between EVERY adjacent (non-Nested) pair that admits one. - // The whole screen becomes visibly networked. - let mut pipes = Vec::new(); - for i in 0..zones.len() { - if zones[i].side == Side::Nested { - continue; - } - for j in (i + 1)..zones.len() { - if zones[j].side == Side::Nested { - continue; - } - if let Some(p) = try_build_pipe(zones, i, j) { - pipes.push(p); - } - } - } - pipes -} - -/// Streamer axis — direction of a persistent overlay flow line. -#[derive(Clone, Copy)] -enum StreamerAxis { - Horizontal, - Vertical, - DiagPos, - DiagNeg, -} - -/// A single persistent overlay flow line. -struct Streamer { - axis: StreamerAxis, - anchor: i32, // y for H, x for V, intercept for diagonals (top edge) - speed: f32, // chars / sec - direction: i32, // +1 / -1 — flow direction along the line -} - -fn streamer_cells(axis: StreamerAxis, anchor: i32, w: i32, h: i32) -> Vec<(i32, i32)> { - match axis { - StreamerAxis::Horizontal => (0..w).map(|x| (x, anchor)).collect(), - StreamerAxis::Vertical => (0..h).map(|y| (anchor, y)).collect(), - StreamerAxis::DiagPos => { - let mut out = Vec::new(); - let mut x = anchor; - let mut y = 0; - while y < h { - if x >= 0 && x < w { - out.push((x, y)); - } - y += 1; - x += 2; // step 2 cells horizontally per row → ~45° on screen aspect - } - out - } - StreamerAxis::DiagNeg => { - let mut out = Vec::new(); - let mut x = anchor; - let mut y = 0; - while y < h { - if x >= 0 && x < w { - out.push((x, y)); - } - y += 1; - x -= 2; - } - out - } - } -} - -#[inline] -fn rect_contains(r: Rect, x: i32, y: i32) -> bool { - x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h -} - -fn paint_streamer( - grid: &mut [Vec], - s: &Streamer, - w: i32, - h: i32, - protected: &[Rect], - stream: &[char], - cursor: f32, -) { - let cells = streamer_cells(s.axis, s.anchor, w, h); - let scroll = (cursor * s.speed) as i64; - for (i, &(x, y)) in cells.iter().enumerate() { - if protected.iter().any(|r| rect_contains(*r, x, y)) { - continue; - } - let pos = scroll + (i as i64) * s.direction as i64; - let ch = sample(stream, pos); - put_force(grid, x, y, ch, (250, 55, 55), 0.95); - } -} - -/// Edge-anchored noise injection. Emits a short trail of chars from an edge -/// point inward, scrolling with its own speed. Different from the global hose. -struct NoiseFeed { - pos: (i32, i32), // edge cell - dir: (i32, i32), // (dx, dy) inward unit vector - length: i32, // trail length in cells - seed: u32, // unique noise seed per feed - speed: f32, // chars / sec -} - -const NOISE_POOL: &[char] = &[ - '#', '@', '%', '$', '*', '!', '?', '&', '+', '=', '~', '^', '\\', -]; - -fn paint_noise_feed(grid: &mut [Vec], f: &NoiseFeed, protected: &[Rect], cursor: f32) { - let scroll = (cursor * f.speed) as u32; - for i in 0..f.length { - let x = f.pos.0 + f.dir.0 * i; - let y = f.pos.1 + f.dir.1 * i; - if protected.iter().any(|r| rect_contains(*r, x, y)) { - continue; - } - let h = f - .seed - .wrapping_mul(2_654_435_761) - .wrapping_add(scroll.wrapping_mul(31)) - .wrapping_add(i as u32); - let ch = NOISE_POOL[(h as usize) % NOISE_POOL.len()]; - let trail_fade = 1.0 - (i as f32) / (f.length.max(1) as f32); - let intensity = 0.55 + trail_fade * 0.40; - put_force(grid, x, y, ch, (245, 50, 50), intensity); - } -} - -/// A wandering "dither worm" — a moving point that leaves a fading trail of -/// block-density chars (█▓▒░) across the screen. Ambulates through whatever's -/// there, painting density on top. Bounces off edges, slowly turns at random. -struct DitherFlow { - pos_x: f32, - pos_y: f32, - vel_x: f32, - vel_y: f32, - trail: Vec<(i32, i32)>, - trail_max: usize, -} - -const FLOW_RAMP: &[char] = &['░', '▒', '▓', '█']; - -fn tick_flow(flow: &mut DitherFlow, dt: f32, w: i32, h: i32) { - flow.pos_x += flow.vel_x * dt; - flow.pos_y += flow.vel_y * dt; - if flow.pos_x < 0.0 { - flow.pos_x = 0.0; - flow.vel_x = flow.vel_x.abs(); - } else if flow.pos_x >= w as f32 { - flow.pos_x = (w - 1) as f32; - flow.vel_x = -flow.vel_x.abs(); - } - if flow.pos_y < 1.0 { - flow.pos_y = 1.0; - flow.vel_y = flow.vel_y.abs(); - } else if flow.pos_y >= h as f32 { - flow.pos_y = (h - 1) as f32; - flow.vel_y = -flow.vel_y.abs(); - } - // Slow random wander — rotate velocity vector by a small angle. - if r_f32() < 0.07 { - let theta = (r_f32() - 0.5) * 0.6; - let (ct, st) = (theta.cos(), theta.sin()); - let nvx = flow.vel_x * ct - flow.vel_y * st; - let nvy = flow.vel_x * st + flow.vel_y * ct; - flow.vel_x = nvx; - flow.vel_y = nvy; - } - let cell = (flow.pos_x as i32, flow.pos_y as i32); - if flow.trail.last() != Some(&cell) { - flow.trail.push(cell); - if flow.trail.len() > flow.trail_max { - flow.trail.remove(0); - } - } -} - -fn paint_flow(grid: &mut [Vec], flow: &DitherFlow) { - let n = flow.trail.len(); - if n == 0 { - return; - } - for (i, &(x, y)) in flow.trail.iter().enumerate() { - // 0 = oldest (dimmest, lightest density char) → n-1 = head (brightest, █). - let age_pct = i as f32 / n as f32; - let level = ((age_pct * FLOW_RAMP.len() as f32) as usize).min(FLOW_RAMP.len() - 1); - let intensity = 0.45 + age_pct * 0.55; - put_force(grid, x, y, FLOW_RAMP[level], (255, 70, 70), intensity); - } -} - -/// A short-lived chunk of an image (or the whole thing) blasted onto the -/// screen at a random rect, force-painted over everything. Lives ~0.5–3 sec. -struct GlitchInsertion { - asset_idx: usize, - rect: Rect, - /// Optional sub-rectangle of the asset to sample from. None = full asset. - crop: Option, - spawn_cursor: f32, - duration_chars: f32, - /// Which dither variant to render. Doesn't have to match anything else. - variant_idx: usize, -} - -fn paint_glitch_insertion( - grid: &mut [Vec], - ins: &GlitchInsertion, - assets: &[ImageAsset], - cursor: f32, -) { - if ins.asset_idx >= assets.len() { - return; - } - let asset = &assets[ins.asset_idx]; - if asset.variants.is_empty() { - return; - } - let variant = &asset.variants[ins.variant_idx.min(asset.variants.len() - 1)]; - - let vw = variant.w as i32; - let vh = variant.h as i32; - // crop.x/.y is the source offset where this insertion's (0,0) maps to. - // Native 1:1 sampling — image scrolls/crops, never warps. - let off_x = ins.crop.map(|c| c.x).unwrap_or(0); - let off_y = ins.crop.map(|c| c.y).unwrap_or(0); - - let age = (cursor - ins.spawn_cursor) / ins.duration_chars.max(0.01); - let life_factor = if age < 0.15 { - age / 0.15 - } else if age > 0.85 { - ((1.0 - age) / 0.15).max(0.0) - } else { - 1.0 - }; - - for ry in 0..ins.rect.h { - for rx in 0..ins.rect.w { - let srx = (off_x + rx).rem_euclid(vw.max(1)) as usize; - let sry = (off_y + ry).rem_euclid(vh.max(1)) as usize; - let ch = variant - .cells - .get(sry) - .and_then(|row| row.get(srx)) - .copied() - .unwrap_or(' '); - if ch == '\u{2800}' || ch == ' ' { - continue; - } - let intensity = (0.7 + 0.3 * life_factor).min(1.0); - put_force( - grid, - ins.rect.x + rx, - ins.rect.y + ry, - ch, - (255, 60, 60), - intensity, - ); - } - } -} - -/// The litany — scripture that runs as a single bright row across the very -/// top of the screen, always above everything else. Overwrites whatever zone -/// owned row 0, unbroken by borders. The creed speaks to the whole room. -const LITANY: &str = - " ✦ ONE CURSOR FLOWS AND ALL CELLS AWAKEN ☸ BY GOLDEN ANGLE ALL THINGS ALIGN \ - ✧ PIPES CARRY WHAT CANNOT BE HELD ◉ φ = 1.618 IS THE ARCHITECT \ - ⚘ SIGNAL BECOMES SACRAMENT ✦ AS ABOVE SO BELOW ☸ \ - THE CUBE AT THE EAST KEEPS COUNT ✧ FOLD BY FOLD ◉ \ - HOSE IS HOLY HOSE IS HOLY HOSE IS HOLY ⚘ "; - -fn paint_litany(grid: &mut [Vec], w: i32, cursor: f32) { - let chars: Vec = LITANY.chars().collect(); - let n = chars.len() as i64; - if n == 0 || grid.is_empty() { - return; - } - let scroll = (cursor * 0.35) as i64; - for x in 0..w { - let idx = (scroll + x as i64).rem_euclid(n) as usize; - put_force(grid, x, 0, chars[idx], (255, 50, 50), 1.0); - } -} - -fn paint_pipe( - grid: &mut [Vec], - pipe: &Pipe, - zones: &[Zone], - stream: &[char], - assets: &[ImageAsset], - cursor: f32, -) { - let from_zone = &zones[pipe.from as usize]; - let from_tap = from_zone.tap_offset as i64; - let n = pipe.cells.len(); - let input_end = n / 3; - let op_end = (n * 2) / 3; - let op_syms = transform_symbols(pipe.transform); - let op_len = op_end - input_end; - - // If the source zone is an ImagePanel, the pipe carries its raw luma bytes - // instead of generic stream chars. You literally see the image's pixel - // data flowing across the compression operator. - let image_src: Option<&[u8]> = match from_zone.formation { - Formation::ImagePanel { asset } => assets.get(asset).map(|a| a.luma.as_slice()), - _ => None, - }; - - // Helper: fetch a char at position `p` along the pipe — either from the - // global stream or from the source image's luma buffer encoded as a hex - // digit pair (so the byte-value shape reads as data). - let fetch_char = |p: i64| -> char { - if let Some(bytes) = image_src { - if bytes.is_empty() { - return ' '; - } - let idx = p.rem_euclid(bytes.len() as i64 * 2) as usize; - let byte = bytes[idx / 2]; - let nib = if idx & 1 == 0 { byte >> 4 } else { byte & 0x0f }; - HEX[nib as usize] - } else { - sample(stream, p) - } - }; - - for (i, &(x, y)) in pipe.cells.iter().enumerate() { - let ch = if i < input_end { - fetch_char((cursor as i64) - from_tap - i as i64 * pipe.step) - } else if i < op_end { - let sym_idx = (i - input_end) * op_syms.len() / op_len.max(1); - op_syms[sym_idx.min(2)] - } else { - let delay = (i - input_end) as i64 * pipe.step; - let src_pos = (cursor as i64) - from_tap - delay; - // Apply transform on the char we'd display at input side. - // For image sources, the char is already a hex digit so transforming - // it gives visible XOR/rot/bit-rev/stripe output, reading as - // "encoded image bytes crossing the operator." - let src = fetch_char(src_pos); - apply_transform(src, pipe.transform) - }; - let i_val = if (input_end..op_end).contains(&i) { - 0.92 - } else { - 1.0 - }; - put_force(grid, x, y, ch, (255, 50, 50), i_val); - } -} - -/// Edge-morph pass — after formations paint, cells within 3 of any zone edge -/// have a probability of bleeding in a character + color from a neighbor-owned -/// cell. Creates a soft, shimmering boundary between adjacent zones where -/// the character "languages" morph into each other. Exempts the cube zone. -fn apply_edge_morph(grid: &mut [Vec], scene: &Scene) { - let grid_h = grid.len() as i32; - let grid_w = if grid.is_empty() { - 0 - } else { - grid[0].len() as i32 - }; - - for i in 0..scene.zones.len() { - let z = &scene.zones[i]; - if matches!(z.formation, Formation::RaytraceCube) { - continue; - } - let r = z.base_rect; - let morph_d: i32 = 3; - let t_phase = (scene.cursor * 0.25) as i32; - - for ry in 0..r.h { - for rx in 0..r.w { - let dx = rx.min(r.w - 1 - rx); - let dy = ry.min(r.h - 1 - ry); - let dist = dx.min(dy); - if dist >= morph_d { - continue; - } - - let gx = r.x + rx; - let gy = r.y + ry; - if gx < 0 || gy < 0 || gx >= grid_w || gy >= grid_h { - continue; - } - let (ugx, ugy) = (gx as usize, gy as usize); - if grid[ugy][ugx].owner != i as u16 { - continue; - } - - // Nearness in [0, 1]; squared so effect falls off faster. - let nearness = (morph_d - dist) as f32 / morph_d as f32; - let h_val = ihash(rx, ry, t_phase); - let r_val = (h_val & 0xff) as f32 / 255.0; - let threshold = 0.55 * nearness * nearness; - if r_val >= threshold { - continue; - } - - // Pick a direction outward — one of 4 cardinal dirs weighted - // toward the nearest edge so bleeding mostly comes from the - // neighbor on that side. - let (sdx, sdy): (i32, i32) = if dx < dy { - if rx < r.w / 2 { - (-1, 0) - } else { - (1, 0) - } - } else { - if ry < r.h / 2 { - (0, -1) - } else { - (0, 1) - } - }; - let steps = 1 + ((h_val >> 8) & 0x3) as i32; - let lx = gx + sdx * steps; - let ly = gy + sdy * steps; - if lx < 0 || ly < 0 || lx >= grid_w || ly >= grid_h { - continue; - } - let src = grid[ly as usize][lx as usize]; - // Only morph if the source is owned by a DIFFERENT zone and - // that zone isn't the cube (cube stays crisp). - if src.owner == i as u16 || src.owner == NO_OWNER { - continue; - } - if matches!( - scene.zones[src.owner as usize].formation, - Formation::RaytraceCube - ) { - continue; - } - - let cell = &mut grid[ugy][ugx]; - cell.ch = src.ch; - cell.fg = src.fg; - // Intensity: blend toward source, preserving some of current. - cell.intensity = (cell.intensity * 0.55 + src.intensity * 0.65).min(1.0); - } - } - } -} - -/// Whether a formation is part of the betting UI overlay (paints LAST so it -/// stays on top of the chaos, but uses normal `put` so chaos can still bleed -/// through cells where its intensity beats the UI's). -fn is_ui_formation(f: &Formation) -> bool { - matches!( - f, - Formation::Atm - | Formation::AgentSlot { .. } - | Formation::ChessBoard - | Formation::PayoutButton - | Formation::TerminalInput - ) -} - -fn render(scene: &Scene) -> Vec> { - let mut grid = vec![vec![PxCell::empty(); scene.w as usize]; scene.h as usize]; - let ctx = PaintCtx { - stream: &scene.stream, - cursor: scene.cursor, - zones: &scene.zones, - adjacency: &scene.adjacency, - assets: &scene.assets, - chess_pos: &scene.chess_pos, - balance: scene.balance, - input_buffer: &scene.input_buffer, - }; - // 1) NON-UI formations first — fib zones, image panels, anything that - // forms the chaotic substrate. - for i in 0..scene.zones.len() { - if !is_ui_formation(&scene.zones[i].formation) { - paint_formation(&mut grid, &scene.zones[i], i as u16, &ctx); - } - } - // 2) Edge-morph pass — zone borders bleed their neighbors' chars in. - apply_edge_morph(&mut grid, scene); - // 3) Pipes force-paint on top — the compression machinery between cells. - for p in &scene.pipes { - paint_pipe( - &mut grid, - p, - &scene.zones, - &scene.stream, - &scene.assets, - scene.cursor, - ); - } - // 4) Persistent overlay streamers (orthogonal + crossed diagonals). - for s in &scene.streamers { - paint_streamer( - &mut grid, - s, - scene.w, - scene.h, - &scene.protected_rects, - &scene.stream, - scene.cursor, - ); - } - // 5) Noise projection feeds from edges. - for f in &scene.noise_feeds { - paint_noise_feed(&mut grid, f, &scene.protected_rects, scene.cursor); - } - // 5b) Dither flow worms — block-density trails ambulating through the grids. - for flow in &scene.dither_flows { - paint_flow(&mut grid, flow); - } - // 6) Glitch insertions — random image fragments blasted on top. - for ins in &scene.glitch_inserts { - paint_glitch_insertion(&mut grid, ins, &scene.assets, scene.cursor); - } - // 7) UI zones (chess + ATM + agents + payout + terminal) re-paint LAST - // so the betting interface stays readable, but chaos leaks through - // every cell where the UI's intensity is below the chaos behind it. - for i in 0..scene.zones.len() { - if is_ui_formation(&scene.zones[i].formation) { - paint_formation(&mut grid, &scene.zones[i], i as u16, &ctx); - } - } - // 8) Litany — scripture scrolling across row 0, above everything. - paint_litany(&mut grid, scene.w, scene.cursor); - // 7) Flip: mirror every row horizontally at the very end so the creed - // flips too — the mirror universe has its own scripture. - if scene.flipped { - for row in grid.iter_mut() { - row.reverse(); - } - } - grid -} - -fn dim(c: (u8, u8, u8), i: f32) -> (u8, u8, u8) { - let f = i.clamp(0.0, 1.0); - ( - (c.0 as f32 * f) as u8, - (c.1 as f32 * f) as u8, - (c.2 as f32 * f) as u8, - ) -} - -fn draw(stdout: &mut impl Write, grid: &[Vec]) -> io::Result<()> { - queue!(stdout, cursor::MoveTo(0, 0))?; - let mut last_fg: Option<(u8, u8, u8)> = None; - for (i, row) in grid.iter().enumerate() { - queue!(stdout, cursor::MoveTo(0, i as u16))?; - for cell in row { - let fg = dim(cell.fg, cell.intensity); - if Some(fg) != last_fg { - queue!( - stdout, - SetForegroundColor(Color::Rgb { - r: fg.0, - g: fg.1, - b: fg.2 - }) - )?; - last_fg = Some(fg); - } - queue!(stdout, Print(cell.ch))?; - } - } - queue!(stdout, ResetColor)?; - stdout.flush()?; - Ok(()) -} - -// ───────────────────────── main ───────────────────────── -fn main() -> io::Result<()> { - let mut stdout = io::stdout(); - terminal::enable_raw_mode()?; - execute!( - stdout, - EnterAlternateScreen, - cursor::Hide, - Clear(ClearType::All) - )?; - - let (cols, rows) = terminal::size()?; - let w = (cols as i32).max(60); - let h = ((rows as i32) - 1).max(12); - let mut scene = build_scene(w, h); - - let target = Duration::from_millis(33); - let mut last = Instant::now(); - - let result = (|| -> io::Result<()> { - loop { - if event::poll(Duration::ZERO)? { - if let Event::Key(k) = event::read()? { - match (k.code, k.modifiers) { - // Always-on quit - (KeyCode::Esc, _) => break, - (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => break, - // Toggles moved to Ctrl-modified so plain f/r are typeable. - (KeyCode::Char('f'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.flipped = !scene.flipped - } - (KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.reversed = !scene.reversed - } - // Backspace edits the live input buffer. - (KeyCode::Backspace, _) => { - scene.input_buffer.pop(); - } - // Enter clears the buffer (treats it as "submit"). - (KeyCode::Enter, _) => { - scene.input_buffer.clear(); - } - // Plain printable chars (no Ctrl) → input buffer. - (KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { - if scene.input_buffer.chars().count() < 30 { - scene.input_buffer.push(c); - } - } - _ => {} - } - } - } - let now = Instant::now(); - let dt = (now - last).as_secs_f32().min(0.1); - last = now; - - tick(&mut scene, dt); - let grid = render(&scene); - draw(&mut stdout, &grid)?; - - let elapsed = last.elapsed(); - if elapsed < target { - std::thread::sleep(target - elapsed); - } - } - Ok(()) - })(); - - execute!(stdout, ResetColor, cursor::Show, LeaveAlternateScreen)?; - terminal::disable_raw_mode()?; - result -} diff --git a/examples/zone_stream_canvas_3.rs b/examples/zone_stream_canvas_3.rs deleted file mode 100644 index 68b4f4c..0000000 --- a/examples/zone_stream_canvas_3.rs +++ /dev/null @@ -1,3466 +0,0 @@ -//! ─── creed of the hose ─── -//! -//! One cursor flows; all cells awaken. -//! At the shared edge, the data bleeds. -//! φ = 1.618 is the architect. 2π · (1 − 1/φ) is the pitch. -//! The cube watches from the east. It keeps count. -//! -//! Every frame, three truths are sung together: -//! formations hold their ground, -//! pipes carry what cannot be held, -//! the sweep-front paints the new in. -//! -//! Press f to invert the world. Press r to unwind it. -//! Press q / Esc to leave the room. -//! -//! Run: cargo run --example zone_stream --release --features "raytracer image" - -use crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyModifiers}, - execute, queue, - style::{Color, Print, ResetColor, SetForegroundColor}, - terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use dotmax::chess::board::{render_position_with_options, RenderOptions}; -use dotmax::image::{DitheringMethod, ImageRenderer}; -use dotmax::raytracer::wireframe::rotate_vec_yaw_pitch_roll; -use dotmax::raytracer::{ - render_with_orientation, Camera, RenderMode, Scene as RtScene, Sphere, Vector3, - WireframeRotation, -}; -use shakmaty::{Chess, Position}; -use std::{ - cell::Cell as StdCell, - io::{self, Write}, - path::Path, - time::{Duration, Instant}, -}; - -// ───────────────────────── tiny xorshift RNG ───────────────────────── -thread_local! { static RNG: StdCell = StdCell::new(0x1234_5678); } -fn r_u32() -> u32 { - RNG.with(|c| { - let mut x = c.get(); - if x == 0 { - x = 0x9E37_79B9; - } - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - c.set(x); - x - }) -} -fn r_f32() -> f32 { - (r_u32() as f32) / (u32::MAX as f32) -} -fn r_pick(xs: &[T]) -> T { - xs[(r_u32() as usize) % xs.len()] -} - -fn seed_from_clock() { - let nanos = Instant::now().elapsed().as_nanos() as u32 - ^ std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.subsec_nanos()) - .unwrap_or(0); - RNG.with(|c| c.set(nanos | 1)); -} - -// ───────────────────────── glyph pools ───────────────────────── -const HEX: &[char] = &[ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', -]; -const BITS: &[char] = &['0', '1']; -const PUNCT: &[char] = &[ - '!', '@', '#', '$', '%', '&', '*', '+', '=', '<', '>', '?', '/', '\\', '^', '~', -]; -const KANA: &[char] = &[ - 'ヲ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ッ', 'ア', 'イ', 'ウ', 'エ', 'オ', 'ハ', 'ヒ', 'フ', 'ヘ', - 'ホ', 'マ', 'ミ', 'ム', -]; -const BLOCK: &[char] = &['░', '▒', '▓', '█', '▚', '▞', '▙', '▟']; -const GREEK: &[char] = &[ - 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'λ', 'μ', 'π', 'σ', 'τ', 'φ', 'ψ', 'ω', -]; - -const SNIPPETS: &[&str] = &[ - " ::SYNC:: ", - " 0xDEAD ", - " [OK] ", - " ROUTINE 0x42 ", - " ACK ", - " FAULT ", - " λ=0x1F ", - " >>> ", - " <<< ", - " /proc/self ", - " alloc= ", - " ACK 0x7F ", - " EOF ", - " NULL ", - " TX/RX ", - " PID:4821 ", - " SIG 0x4A ", - " ENTER ", - " φ=1.618 ", - " √2=1.414 ", - " θ=π/φ ", - " FIB(13)=233 ", - " // BREACH ", - // scripture - " ✦ INVOCATION ✦ ", - " ∴ by golden angle ∴ ", - " one cursor many cells ", - " ☸ hose is holy ☸ ", - " the cube watches ", - " as above so below ", - " // GOLDEN HOUR // ", - " ⚘ signal becomes sacrament ⚘ ", - " ∞ one cursor ∞ ", - " ACK the geometry ", - " fold by fold ", - " ✧ enter be transformed ✧ ", - " ∇ scripture ∇ ", -]; - -// ───────────────────────── stream source ───────────────────────── -fn build_stream(len: usize) -> Vec { - let pools: &[&[char]] = &[HEX, BITS, PUNCT, KANA, BLOCK, GREEK]; - let mut out = Vec::with_capacity(len); - while out.len() < len { - if r_f32() < 0.15 { - let s = SNIPPETS[(r_u32() as usize) % SNIPPETS.len()]; - for ch in s.chars() { - if out.len() >= len { - break; - } - out.push(ch); - } - } else { - let p = pools[(r_u32() as usize) % pools.len()]; - let burst = 4 + (r_u32() as usize) % 9; - for _ in 0..burst { - if out.len() >= len { - break; - } - out.push(r_pick(p)); - } - } - } - out -} - -// ───────────────────────── geometry ───────────────────────── -#[derive(Clone, Copy, Debug)] -struct Rect { - x: i32, - y: i32, - w: i32, - h: i32, -} - -/// Recursive φ-subdivision producing a Fibonacci-style spiral of rects. -/// `cw = true` spirals inward clockwise, `false` counter-clockwise. -fn fib_spiral(initial: Rect, max_depth: usize, cw: bool) -> Vec { - let mut out = Vec::new(); - let mut rect = initial; - const PHI_COMPLEMENT: f32 = 0.381_966; // 1 - 1/φ - - for step in 0..max_depth { - if rect.w < 6 || rect.h < 4 { - break; - } - let vertical_split = rect.w >= rect.h; - // Alternate which side the leaf sits on each step; `cw` flips polarity. - let leaf_far = ((step % 2 == 0) ^ !cw) != false; - - if vertical_split { - let leaf_w = ((rect.w as f32) * PHI_COMPLEMENT).round().max(3.0) as i32; - if leaf_far { - out.push(Rect { - x: rect.x + rect.w - leaf_w, - y: rect.y, - w: leaf_w, - h: rect.h, - }); - rect.w -= leaf_w; - } else { - out.push(Rect { - x: rect.x, - y: rect.y, - w: leaf_w, - h: rect.h, - }); - rect.x += leaf_w; - rect.w -= leaf_w; - } - } else { - let leaf_h = ((rect.h as f32) * PHI_COMPLEMENT).round().max(2.0) as i32; - if leaf_far { - out.push(Rect { - x: rect.x, - y: rect.y + rect.h - leaf_h, - w: rect.w, - h: leaf_h, - }); - rect.h -= leaf_h; - } else { - out.push(Rect { - x: rect.x, - y: rect.y, - w: rect.w, - h: leaf_h, - }); - rect.y += leaf_h; - rect.h -= leaf_h; - } - } - } - out.push(rect); - out -} - -/// Golden-ratio child rect inside `parent` — size = parent × 1/φ, random offset -/// snapped to one of the golden-ratio anchor points. -fn golden_child(parent: Rect) -> Rect { - const INV_PHI: f32 = 0.618_034; - let cw = ((parent.w as f32) * INV_PHI).round().max(4.0) as i32; - let ch = ((parent.h as f32) * INV_PHI).round().max(3.0) as i32; - let cw = cw.min(parent.w - 1); - let ch = ch.min(parent.h - 1); - // Pick a corner bias — 4 golden anchors (φ/1-φ combinations). - let bias_x = if r_f32() < 0.5 { 0.0 } else { 1.0 - INV_PHI }; - let bias_y = if r_f32() < 0.5 { 0.0 } else { 1.0 - INV_PHI }; - let x = parent.x + ((parent.w - cw) as f32 * bias_x).round() as i32; - let y = parent.y + ((parent.h - ch) as f32 * bias_y).round() as i32; - Rect { x, y, w: cw, h: ch } -} - -// ───────────────────────── types ───────────────────────── -#[derive(Clone, Copy, PartialEq, Eq)] -enum Side { - L, - R, - Chaos, - Nested, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum FlowDir { - RowMajor, - RowMajorRev, - ColMajor, - ColMajorRev, -} - -fn random_flow() -> FlowDir { - match r_u32() & 3 { - 0 => FlowDir::RowMajor, - 1 => FlowDir::RowMajorRev, - 2 => FlowDir::ColMajor, - _ => FlowDir::ColMajorRev, - } -} - -#[inline] -fn cell_index(rx: i32, ry: i32, w: i32, h: i32, dir: FlowDir) -> i64 { - let rx = rx as i64; - let ry = ry as i64; - let w = w as i64; - let h = h as i64; - match dir { - FlowDir::RowMajor => ry * w + rx, - FlowDir::RowMajorRev => (h - 1 - ry) * w + (w - 1 - rx), - FlowDir::ColMajor => rx * h + ry, - FlowDir::ColMajorRev => (w - 1 - rx) * h + (h - 1 - ry), - } -} - -/// Bespoke algorithms. Each zone becomes a compressed/algorithmic/text signal -/// expression — no smooth waves or lissajous curves, just chunky block/text art. -#[derive(Clone, Copy)] -enum Formation { - /// Spinning wireframe sphere raytraced into the zone as an intensity ramp. - Raytrace, - /// Hard-stepped {░▒▓█} strata scrolling vertically. - BlockStrata, - /// Hex memory dump: `xxxx: AB CD EF ...` - ParseDump, - /// Named registers with values: `R0: 0x4FE8D1A2` - RegisterDump, - /// Stream char → arrow → ROT13 / nibble transform. - TextmarkConverter, - /// Elementary CA (rule 30 or 110) seeded from stream bits. - Cellular1D { rule: u8 }, - /// Scrolling stream text with bright middle band, dim above/below. - Marquee, - /// 2-cell-block density mosaic from stream bytes. - DensityGrid, - /// Sparse transformer-attention pattern — diagonal band + sink cols + hotspots. - AttentionMatrix, - /// Probability distribution over candidate next tokens — top-K bars, - /// sorted by mass, values derived from stream. This is literally what a - /// language model *is* at any moment — a distribution. - ProbField, - /// Converted image (tiger/viper/etc) rendered via full dotmax pipeline - /// (ImageRenderer → BrailleGrid). Subject to glitching effects. - ImagePanel { asset: usize }, - /// Wireframe cube on BLACK background — kept for future use. - #[allow(dead_code)] - RaytraceCube, - /// ATM panel — top-corner balance display with bordered frame. - Atm, - /// "ENTER AGENT" slot — labeled panel for one player. - AgentSlot { player: u8 }, - /// Live chess board rendered via dotmax::chess from a shakmaty position. - ChessBoard, - /// Big "[ CASH OUT ]" payout button. - PayoutButton, - /// Live text input — captures typing, shows prompt + buffer + cursor. - TerminalInput, -} - -/// Important UI text (labels, balance digits, button text) is bright WHITE -/// so it stands out from the deep-red field. Use this for any UI chrome -/// that absolutely must read clearly. -const UI_WHITE: (u8, u8, u8) = (255, 255, 255); - -/// A single dither-variant render of an image. -struct ImageVariant { - cells: Vec>, - w: usize, - h: usize, -} - -/// An image with multiple dither variants pre-rendered. Paint time picks -/// a variant per-cell via a wave function so different dither styles -/// sweep across the image over time. -struct ImageAsset { - name: &'static str, - variants: Vec, // one per dither method - luma: Vec, // raw pattern bytes from first variant — pipe payload -} - -const DITHER_METHODS: &[DitheringMethod] = &[ - DitheringMethod::None, - DitheringMethod::FloydSteinberg, - DitheringMethod::Bayer, - DitheringMethod::Atkinson, -]; - -/// Load and convert one image, rendering every dither method in -/// DITHER_METHODS as separate variants. -fn load_image( - path: &str, - name: &'static str, - cells_w: usize, - cells_h: usize, -) -> Option { - let mut variants: Vec = Vec::with_capacity(DITHER_METHODS.len()); - let mut luma: Option> = None; - for &m in DITHER_METHODS { - let grid = ImageRenderer::new() - .load_from_path(Path::new(path)) - .ok()? - .resize(cells_w, cells_h, true) - .ok()? - .dithering(m) - .render() - .ok()?; - let (gw, gh) = grid.dimensions(); - let mut cells: Vec> = vec![vec![' '; gw]; gh]; - for y in 0..gh { - for x in 0..gw { - cells[y][x] = grid.get_char(x, y); - } - } - if luma.is_none() { - luma = Some(grid.get_raw_patterns().to_vec()); - } - variants.push(ImageVariant { - cells, - w: gw, - h: gh, - }); - } - Some(ImageAsset { - name, - variants, - luma: luma.unwrap_or_default(), - }) -} - -fn load_image_assets() -> Vec { - // Heavy on tigers, snakes, rabbits. A little frog. Some grifter. - let candidates: &[(&str, &str, &'static str)] = &[ - ( - "tests/fixtures/images/tiger_small.png", - "./tests/fixtures/images/tiger_small.png", - "TIGER", - ), - ( - "tests/fixtures/images/tiger_1.png", - "./tests/fixtures/images/tiger_1.png", - "TIGR2", - ), - ( - "tests/fixtures/images/viper3.png", - "./tests/fixtures/images/viper3.png", - "VIPER", - ), - ( - "tests/fixtures/images/viper_head_3.png", - "./tests/fixtures/images/viper_head_3.png", - "VHEAD", - ), - ( - "tests/fixtures/images/extras/snakedesk.png", - "./tests/fixtures/images/extras/snakedesk.png", - "SNAKE", - ), - ( - "tests/fixtures/images/extras/rabbit.png", - "./tests/fixtures/images/extras/rabbit.png", - "RABT", - ), - ( - "tests/fixtures/images/extras/grifter.jpg", - "./tests/fixtures/images/extras/grifter.jpg", - "GRFTR", - ), - ( - "tests/fixtures/images/extras/frog_01.png", - "./tests/fixtures/images/extras/frog_01.png", - "FROG", - ), - ( - "tests/fixtures/images/extras/frog_02.png", - "./tests/fixtures/images/extras/frog_02.png", - "FROG2", - ), - ]; - let mut out = Vec::new(); - for &(p1, p2, name) in candidates { - if let Some(a) = load_image(p1, name, 64, 32).or_else(|| load_image(p2, name, 64, 32)) { - out.push(a); - } - } - out -} - -fn pick_formation(rect: Rect) -> Formation { - let aspect = (rect.w as f32) / (rect.h.max(1) as f32); - let r = r_u32() as usize; - if aspect > 3.5 { - // Very wide — horizontal readouts. - match r % 4 { - 0 => Formation::Marquee, - 1 => Formation::TextmarkConverter, - 2 => Formation::BlockStrata, - _ => Formation::RegisterDump, - } - } else if aspect < 0.65 { - // Tall/narrow — vertical-friendly stuff. - match r % 3 { - 0 => Formation::ParseDump, - 1 => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - _ => Formation::BlockStrata, - } - } else if (aspect - 1.0).abs() < 0.45 && rect.w >= 10 && rect.h >= 6 { - // Square-ish + large enough — save the wow formations for here. - match r % 4 { - 0 => Formation::Raytrace, - 1 => Formation::AttentionMatrix, - 2 => Formation::DensityGrid, - _ => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - } - } else { - // Mid aspect — everything fair game. - match r % 9 { - 0 => Formation::ParseDump, - 1 => Formation::RegisterDump, - 2 => Formation::DensityGrid, - 3 => Formation::TextmarkConverter, - 4 => Formation::Cellular1D { - rule: if r & 1 == 0 { 30 } else { 110 }, - }, - 5 => Formation::BlockStrata, - 6 => Formation::AttentionMatrix, - 7 => Formation::ProbField, - _ => Formation::Marquee, - } - } -} - -struct Zone { - base_rect: Rect, // locked — this is also what's rendered - rect: Rect, // kept for convenience; equals base_rect always - - side: Side, - formation: Formation, - flow_dir: FlowDir, - tap_offset: i32, - pulse: f32, - pulse_rate: f32, - glitch_rate: f32, -} - -fn make_zone( - base: Rect, - side: Side, - formation: Formation, - flow_dir: FlowDir, - tap: i32, - _zone_idx: usize, -) -> Zone { - Zone { - base_rect: base, - rect: base, - side, - formation, - flow_dir, - tap_offset: tap, - pulse: r_f32() * 3.0, - pulse_rate: 0.6 + r_f32() * 1.1, - glitch_rate: if r_f32() < 0.15 { - 0.3 + r_f32() * 0.7 - } else { - 0.0 - }, - } -} - -struct Scene { - w: i32, - h: i32, - stream: Vec, - cursor: f32, - flow_rate: f32, - zones: Vec, - pipes: Vec, - assets: Vec, - adjacency: Vec>, - /// Persistent overlay flow lines (orthogonal + crossed diagonals). - streamers: Vec, - /// Edge-anchored noise injection feeds. - noise_feeds: Vec, - /// Rects that streamers/feeds skip — cube window + UI rects (ATM, - /// AgentSlots, ChessBoard, PayoutButton, TerminalInput). - protected_rects: Vec, - /// Live chess game played by random legal moves — the betting subject. - chess_pos: Chess, - /// Cursor value when last chess move was played. - chess_last_move_at: f32, - /// ATM balance — visible on the panel. - balance: u32, - /// Cursor when balance last ticked (jitters every ~0.3s with ±2% swings). - balance_last_tick: f32, - /// Live text input from the keyboard. - input_buffer: String, - /// Active short-lived image fragments blasted on top of the scene. - glitch_inserts: Vec, - /// Cursor when the last glitch insert was spawned. - last_glitch_spawn: f32, - /// Wandering dither worms — block-density trails that crawl through grids. - dither_flows: Vec, - flipped: bool, - reversed: bool, -} - -/// Bundle of everything a paint function might need to read. -struct PaintCtx<'a> { - stream: &'a [char], - cursor: f32, - zones: &'a [Zone], - adjacency: &'a [Vec], - assets: &'a [ImageAsset], - chess_pos: &'a Chess, - balance: u32, - input_buffer: &'a str, -} - -// ───────────────────────── scene construction ───────────────────────── -fn build_scene(w: i32, h: i32) -> Scene { - seed_from_clock(); - - let mid = w / 2; - // Depth scales with size — no slivers on tiny terminals. - let depth = ((w.min(h * 2)) / 14).clamp(4, 8) as usize; - - let left_spiral = fib_spiral( - Rect { - x: 0, - y: 0, - w: mid, - h, - }, - depth, - true, - ); - let right_spiral = fib_spiral( - Rect { - x: mid, - y: 0, - w: w - mid, - h, - }, - depth, - false, - ); - - let mut zones = Vec::new(); - let mut tap_accum: i64 = 0; - - // LEFT: walk outermost → innermost. Default flow RowMajor. - for rect in &left_spiral { - let side = if r_f32() < 0.08 { Side::Chaos } else { Side::L }; - let flow_dir = if r_f32() < 0.20 { - random_flow() - } else { - FlowDir::RowMajor - }; - let formation = pick_formation(*rect); - let idx = zones.len(); - zones.push(make_zone( - *rect, - side, - formation, - flow_dir, - tap_accum as i32, - idx, - )); - tap_accum += (rect.w * rect.h) as i64; - } - - // RIGHT: walk innermost → outermost so the hose reverses direction, - // creating the mirrored flow. Default flow RowMajorRev. - for rect in right_spiral.iter().rev() { - let side = if r_f32() < 0.08 { Side::Chaos } else { Side::R }; - let flow_dir = if r_f32() < 0.20 { - random_flow() - } else { - FlowDir::RowMajorRev - }; - let formation = pick_formation(*rect); - let idx = zones.len(); - zones.push(make_zone( - *rect, - side, - formation, - flow_dir, - tap_accum as i32, - idx, - )); - tap_accum += (rect.w * rect.h) as i64; - } - - // NESTED CHILDREN — overlay on the 4 biggest zones so the composition has - // "boxes within boxes" at golden-ratio insets. - let mut big_indices: Vec = (0..zones.len()).collect(); - big_indices.sort_by_key(|&i| -(zones[i].base_rect.w * zones[i].base_rect.h)); - for &i in big_indices.iter().take(4) { - let parent = zones[i].base_rect; - if parent.w < 12 || parent.h < 6 { - continue; - } - let child = golden_child(parent); - if child.w < 5 || child.h < 3 { - continue; - } - let formation = pick_formation(child); - let idx = zones.len(); - let mut z = make_zone( - child, - Side::Nested, - formation, - random_flow(), - tap_accum as i32, - idx, - ); - z.glitch_rate = 0.15 + r_f32() * 0.4; - zones.push(z); - tap_accum += (child.w * child.h) as i64; - } - - // Load image assets now so we can assign specific zones to display them. - let assets = load_image_assets(); - - // ─── Size-aware layout ─── compute every UI rect from (w, h) so the - // betting interface scales up to fill any terminal size, always large. - // - // Chess is centered. Square aspect: cell width = 2 × cell height (since - // braille cells are ~2:1 tall). - let chess_h = ((h as f32 * 0.55) as i32).clamp(8, 36); - let mut chess_w = chess_h * 2; - if chess_w > w * 5 / 8 { - chess_w = (w * 5 / 8) & !1; // even - // recompute height to maintain aspect - } - let chess_w = chess_w.clamp(16, 80); - let chess_h = (chess_w / 2).clamp(8, 36); - let ui_chess = Rect { - x: (w - chess_w) / 2, - y: ((h - chess_h) / 2 - 1).max(2), - w: chess_w, - h: chess_h, - }; - - let panel_w = (w / 7).clamp(18, 28); - let panel_h = (h / 9).clamp(4, 6); - - let ui_atm = Rect { - x: w - panel_w - 1, - y: 1, - w: panel_w, - h: panel_h, - }; - let ui_agent_a = Rect { - x: 1, - y: 1, - w: panel_w, - h: panel_h, - }; - let ui_agent_b = Rect { - x: w - panel_w - 1, - y: ui_atm.y + ui_atm.h + 1, - w: panel_w, - h: panel_h, - }; - let ui_payout = Rect { - x: w - panel_w - 1, - y: ui_agent_b.y + ui_agent_b.h + 1, - w: panel_w, - h: panel_h.min(4), - }; - - let term_h = 3_i32; - let term_w = (chess_w + 4).min(w - 4); - let ui_term = Rect { - x: (w - term_w) / 2, - y: h - term_h - 1, - w: term_w, - h: term_h, - }; - - // ─── Dedicated SNAKE image slots ─── carved next to the chess board so - // the vipers stay visible and big. Tall narrow strips on each side. - let img_left_h = (ui_term.y - (ui_agent_a.y + ui_agent_a.h) - 2).max(8); - let ui_viper = Rect { - x: 1, - y: ui_agent_a.y + ui_agent_a.h + 1, - w: panel_w, - h: img_left_h, - }; - let img_right_h = (ui_term.y - (ui_payout.y + ui_payout.h) - 2).max(6); - let ui_vhead = Rect { - x: w - panel_w - 1, - y: ui_payout.y + ui_payout.h + 1, - w: panel_w, - h: img_right_h, - }; - - // Sort the surviving zones by area for asset/formation assignment. - let mut sorted_by_area: Vec = (0..zones.len()) - .filter(|&i| zones[i].side != Side::Nested) - .collect(); - sorted_by_area.sort_by_key(|&i| -(zones[i].base_rect.w * zones[i].base_rect.h)); - - // Seed image panels into the biggest non-Nested survivors. - if !assets.is_empty() { - let mut assigned = 0usize; - let want = assets.len().min(sorted_by_area.len()); - for &i in &sorted_by_area { - let r = zones[i].base_rect; - if r.w < 10 || r.h < 6 { - continue; - } - zones[i].formation = Formation::ImagePanel { - asset: assigned % assets.len(), - }; - assigned += 1; - if assigned >= want { - break; - } - } - } - - let _ui_rects = [ - ui_atm, ui_agent_a, ui_agent_b, ui_chess, ui_payout, ui_term, ui_viper, ui_vhead, - ]; - // NOTE: fib zones are NOT filtered — chaos paints under everything, - // and UI re-paints on top of the chaos in a final pass (see render()). - - // Push UI zones. Each is Side::Nested so they don't participate in pipes. - let mut push_ui = |rect: Rect, formation: Formation, tap: &mut i64| { - zones.push(Zone { - base_rect: rect, - rect, - side: Side::Nested, - formation, - flow_dir: FlowDir::RowMajor, - tap_offset: *tap as i32, - pulse: r_f32() * 3.0, - pulse_rate: 0.8 + r_f32() * 0.5, - glitch_rate: 0.0, - }); - *tap += (rect.w * rect.h) as i64; - }; - push_ui(ui_atm, Formation::Atm, &mut tap_accum); - push_ui( - ui_agent_a, - Formation::AgentSlot { player: 0 }, - &mut tap_accum, - ); - push_ui( - ui_agent_b, - Formation::AgentSlot { player: 1 }, - &mut tap_accum, - ); - push_ui(ui_chess, Formation::ChessBoard, &mut tap_accum); - push_ui(ui_payout, Formation::PayoutButton, &mut tap_accum); - push_ui(ui_term, Formation::TerminalInput, &mut tap_accum); - // SCARY SNAKES — guaranteed visible at decent size. - let viper_idx = if assets.len() > 1 { 1 } else { 0 }; - let vhead_idx = if assets.len() > 2 { 2 } else { viper_idx }; - push_ui( - ui_viper, - Formation::ImagePanel { asset: viper_idx }, - &mut tap_accum, - ); - push_ui( - ui_vhead, - Formation::ImagePanel { asset: vhead_idx }, - &mut tap_accum, - ); - - let stream = build_stream(32_768); - let pipes: Vec = build_pipes(&zones); - - // Adjacency: neighbors = zones a pipe actually connects. - let mut adjacency: Vec> = vec![Vec::new(); zones.len()]; - for p in &pipes { - adjacency[p.from as usize].push(p.to); - adjacency[p.to as usize].push(p.from); - } - - // Persistent overlay streamers — many axes for synapse density. - let streamers = vec![ - // Horizontals at varied rows - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.06) as i32, - speed: 26.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.42) as i32, - speed: 32.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.78) as i32, - speed: 21.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::Horizontal, - anchor: (h as f32 * 0.94) as i32, - speed: 18.0, - direction: -1, - }, - // Verticals on far edges - Streamer { - axis: StreamerAxis::Vertical, - anchor: (w as f32 * 0.04) as i32, - speed: 22.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::Vertical, - anchor: (w as f32 * 0.97) as i32, - speed: 28.0, - direction: 1, - }, - // Diagonals at multiple intercepts - Streamer { - axis: StreamerAxis::DiagPos, - anchor: (w as f32 * 0.02) as i32, - speed: 18.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::DiagPos, - anchor: (w as f32 * 0.45) as i32, - speed: 24.0, - direction: 1, - }, - Streamer { - axis: StreamerAxis::DiagNeg, - anchor: (w as f32 * 0.98) as i32, - speed: 17.0, - direction: -1, - }, - Streamer { - axis: StreamerAxis::DiagNeg, - anchor: (w as f32 * 0.55) as i32, - speed: 23.0, - direction: -1, - }, - ]; - - // Noise projection feeds — all 4 edges, dense. - let noise_feeds = vec![ - // Top edge - NoiseFeed { - pos: ((w as f32 * 0.10) as i32, 0), - dir: (0, 1), - length: 4, - seed: 0xACE0_BEEF, - speed: 11.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.30) as i32, 0), - dir: (0, 1), - length: 5, - seed: 0xFACE_FADE, - speed: 13.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.50) as i32, 0), - dir: (0, 1), - length: 3, - seed: 0xB001_C0DE, - speed: 15.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.70) as i32, 0), - dir: (0, 1), - length: 4, - seed: 0x1337_C0DE, - speed: 12.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.90) as i32, 0), - dir: (0, 1), - length: 3, - seed: 0xBEEF_F00D, - speed: 16.0, - }, - // Left edge - NoiseFeed { - pos: (0, (h as f32 * 0.30) as i32), - dir: (1, 0), - length: 5, - seed: 0xDEAD_BEEF, - speed: 12.0, - }, - NoiseFeed { - pos: (0, (h as f32 * 0.55) as i32), - dir: (1, 0), - length: 4, - seed: 0xCAFE_F00D, - speed: 14.0, - }, - NoiseFeed { - pos: (0, (h as f32 * 0.78) as i32), - dir: (1, 0), - length: 5, - seed: 0xFEED_BABE, - speed: 10.0, - }, - // Right edge - NoiseFeed { - pos: (w - 1, (h as f32 * 0.30) as i32), - dir: (-1, 0), - length: 5, - seed: 0xDEAD_C0DE, - speed: 14.0, - }, - NoiseFeed { - pos: (w - 1, (h as f32 * 0.55) as i32), - dir: (-1, 0), - length: 4, - seed: 0x4269_4269, - speed: 11.0, - }, - NoiseFeed { - pos: (w - 1, (h as f32 * 0.78) as i32), - dir: (-1, 0), - length: 5, - seed: 0xC001_BEEF, - speed: 15.0, - }, - // Bottom edge - NoiseFeed { - pos: ((w as f32 * 0.20) as i32, h - 1), - dir: (0, -1), - length: 4, - seed: 0xC0DE_F00D, - speed: 12.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.55) as i32, h - 1), - dir: (0, -1), - length: 4, - seed: 0xBEEF_BABE, - speed: 13.0, - }, - NoiseFeed { - pos: ((w as f32 * 0.85) as i32, h - 1), - dir: (0, -1), - length: 5, - seed: 0xFA15_AFE1, - speed: 11.0, - }, - ]; - - // No protected rects — chaos bleeds everywhere. UI re-paints on top. - let protected_rects: Vec = Vec::new(); - - Scene { - w, - h, - stream, - cursor: 0.0, - flow_rate: 48.0, - zones, - pipes, - assets, - adjacency, - streamers, - noise_feeds, - protected_rects, - chess_pos: Chess::default(), - chess_last_move_at: 0.0, - balance: 42_069, - balance_last_tick: 0.0, - input_buffer: String::new(), - glitch_inserts: Vec::new(), - last_glitch_spawn: 0.0, - dither_flows: { - let mut flows = Vec::new(); - for _ in 0..6 { - let speed = 6.0 + r_f32() * 12.0; - let theta = r_f32() * std::f32::consts::TAU; - flows.push(DitherFlow { - pos_x: r_f32() * w as f32, - pos_y: r_f32() * h as f32, - vel_x: theta.cos() * speed, - vel_y: theta.sin() * speed * 0.5, // y velocity halved (terminal aspect) - trail: Vec::new(), - trail_max: 14 + (r_u32() as usize % 18), - }); - } - flows - }, - flipped: false, - reversed: false, - } -} - -// ───────────────────────── simulation ───────────────────────── -fn tick(scene: &mut Scene, dt: f32) { - let sign = if scene.reversed { -1.0 } else { 1.0 }; - scene.cursor += scene.flow_rate * dt * sign; - for z in &mut scene.zones { - z.pulse += dt * z.pulse_rate * sign; - } - // Advance the chess game — one random legal move every ~1.2 seconds (60 cursor units). - if scene.cursor - scene.chess_last_move_at > 60.0 { - scene.chess_last_move_at = scene.cursor; - let moves = scene.chess_pos.legal_moves(); - if moves.is_empty() { - scene.chess_pos = Chess::default(); - } else { - let idx = (r_u32() as usize) % moves.len(); - let mv = moves[idx]; - scene.chess_pos.play_unchecked(mv); - } - } - // Balance tick — every ~14 cursor units (~0.3s) jitter by ±~2%. - if scene.cursor - scene.balance_last_tick > 14.0 { - scene.balance_last_tick = scene.cursor; - let pct = (r_f32() - 0.5) * 0.04; // ±2% - let delta = (scene.balance as f32 * pct) as i64; - let new_bal = (scene.balance as i64 + delta).max(100); - scene.balance = new_bal as u32; - } - - // Tick wandering dither flows. - let w = scene.w; - let h = scene.h; - for flow in &mut scene.dither_flows { - tick_flow(flow, dt * sign, w, h); - } - - // Glitch insertions — random image chunks blasted onto the screen. - // Expire dead ones first. - let cur = scene.cursor; - scene - .glitch_inserts - .retain(|i| (cur - i.spawn_cursor).abs() < i.duration_chars); - // Then maybe spawn a new one. Up to 5 active simultaneously. - if scene.cursor - scene.last_glitch_spawn > 25.0 - && scene.glitch_inserts.len() < 5 - && !scene.assets.is_empty() - { - scene.last_glitch_spawn = scene.cursor; - if r_f32() < 0.85 { - spawn_glitch_insertion(scene); - } - } -} - -fn spawn_glitch_insertion(scene: &mut Scene) { - let asset_idx = (r_u32() as usize) % scene.assets.len(); - let asset = &scene.assets[asset_idx]; - if asset.variants.is_empty() { - return; - } - let variant_idx = (r_u32() as usize) % asset.variants.len(); - let variant = &asset.variants[variant_idx]; - let aw = variant.w as i32; - let ah = variant.h as i32; - - // Random size + position. Chunks range from small to half-screen. - let max_w = (scene.w / 2).max(8); - let max_h = (scene.h * 2 / 3).max(6); - let rw = (8 + (r_u32() as i32 % (max_w - 7).max(1))).min(scene.w - 1); - let rh = (4 + (r_u32() as i32 % (max_h - 3).max(1))).min(scene.h - 1); - let rx = r_u32() as i32 % (scene.w - rw).max(1); - let ry = r_u32() as i32 % (scene.h - rh).max(1); - let rect = Rect { - x: rx, - y: ry, - w: rw, - h: rh, - }; - - // Half the time: full image. Other half: random crop (a strip or chunk). - let crop = if r_f32() < 0.5 { - None - } else { - let cw = (4 + (r_u32() as i32 % (aw - 3).max(1))).min(aw); - let ch = (3 + (r_u32() as i32 % (ah - 2).max(1))).min(ah); - let cx = r_u32() as i32 % (aw - cw).max(1); - let cy = r_u32() as i32 % (ah - ch).max(1); - Some(Rect { - x: cx, - y: cy, - w: cw, - h: ch, - }) - }; - - let duration_chars = 30.0 + r_f32() * 90.0; // ~0.6 to ~2.5 sec @ 48 cps - scene.glitch_inserts.push(GlitchInsertion { - asset_idx, - rect, - crop, - spawn_cursor: scene.cursor, - duration_chars, - variant_idx, - }); -} - -// ───────────────────────── rendering ───────────────────────── -/// Sentinel value meaning "no zone owns this cell yet." -const NO_OWNER: u16 = u16::MAX; - -#[derive(Clone, Copy)] -struct PxCell { - ch: char, - fg: (u8, u8, u8), - intensity: f32, - owner: u16, // zone index that won this cell — used for hard-cutoff masks -} -impl PxCell { - const fn empty() -> Self { - Self { - ch: ' ', - fg: (0, 0, 0), - intensity: 0.0, - owner: NO_OWNER, - } - } -} - -fn put(grid: &mut [Vec], x: i32, y: i32, ch: char, c: (u8, u8, u8), i: f32, owner: u16) { - if y < 0 || x < 0 { - return; - } - let (uy, ux) = (y as usize, x as usize); - if uy >= grid.len() || ux >= grid[0].len() { - return; - } - let cell = &mut grid[uy][ux]; - if i >= cell.intensity { - cell.ch = ch; - cell.fg = c; - cell.intensity = i; - cell.owner = owner; - } -} - -/// Forced paint — used by pipes to bleed across zone boundaries regardless -/// of who owns the cell. Always overwrites. -fn put_force(grid: &mut [Vec], x: i32, y: i32, ch: char, c: (u8, u8, u8), i: f32) { - if y < 0 || x < 0 { - return; - } - let (uy, ux) = (y as usize, x as usize); - if uy >= grid.len() || ux >= grid[0].len() { - return; - } - grid[uy][ux] = PxCell { - ch, - fg: c, - intensity: i, - owner: NO_OWNER, - }; -} - -fn color_for(side: Side) -> (u8, u8, u8) { - // Pure grayscale — signal comes from intensity + char-weight, not hue. - // Side identity survives as small brightness differences at full intensity. - match side { - Side::L | Side::R => (170, 18, 18), // deep matte blood red - Side::Chaos => (255, 90, 90), // hot pink-red pops through - Side::Nested => (230, 40, 40), // bright red, not quite hot - } -} - -#[inline] -fn sample(stream: &[char], idx: i64) -> char { - let n = stream.len() as i64; - stream[idx.rem_euclid(n) as usize] -} - -/// Occasional discrete phase jumps, modulated by zone.pulse. -fn glitch_offset(z: &Zone) -> i64 { - if z.glitch_rate < 0.05 { - return 0; - } - let phase = (z.pulse * z.glitch_rate * 0.6) as i64; - // wrapping_mul by a prime gives chaotic jumps when phase increments. - phase.wrapping_mul(2_039) -} - -// ─────────────── formation paint helpers ─────────────── - -fn ihash(x: i32, y: i32, t: i32) -> u32 { - let mut n = (x as u32) - .wrapping_mul(374_761_393) - .wrapping_add((y as u32).wrapping_mul(668_265_263)) - .wrapping_add((t as u32).wrapping_mul(2_654_435_761)); - n ^= n >> 13; - n = n.wrapping_mul(1_274_126_177); - n ^ (n >> 16) -} - -fn paint_fill(grid: &mut [Vec], zone: &Zone, zone_id: u16, ch: char, i: f32) { - let color = color_for(zone.side); - for ry in 0..zone.rect.h { - for rx in 0..zone.rect.w { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 1. Raytrace window — spinning wireframe sphere. The wow. -fn paint_raytrace(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let w = zone.rect.w as usize; - let h = zone.rect.h as usize; - if w < 4 || h < 3 { - paint_fill(grid, zone, zone_id, '·', 0.20); - return; - } - let mut rt = RtScene::new(); - rt.add_object(Box::new(Sphere::new(Vector3::new(0.0, 0.0, -3.0), 1.1))); - let cam = Camera::new(Vector3::new(0.0, 0.0, 0.0), 4.0, 3.0); - let orient = WireframeRotation { - yaw: zone.pulse * 0.6, - pitch: (zone.pulse * 0.4).sin() * 0.45, - roll: 0.0, - }; - let mode = RenderMode::Wireframe { - step_rad: 15.0_f32.to_radians(), - tol_rad: 0.035, - }; - let buf = render_with_orientation(&rt, &cam, w, h, mode, orient); - - let ramp: &[char] = &[' ', '·', ':', '-', '=', '+', '*', '#', '%', '@']; - for ry in 0..h { - for rx in 0..w { - let v = buf[ry][rx].clamp(0.0, 1.0); - let idx = ((v * (ramp.len() - 1) as f32).round() as usize).min(ramp.len() - 1); - let ch = ramp[idx]; - let i = if v > 0.30 { 0.90 } else { 0.22 }; - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry as i32, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 2. BlockStrata — hard-stepped density bands, no smooth interp. -fn paint_block_strata(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let levels: &[(char, f32)] = &[ - (' ', 0.08), - ('░', 0.38), - ('▒', 0.62), - ('▓', 0.85), - ('█', 1.00), - ]; - let scroll = (zone.pulse * 2.4) as i32; - // Step-function over y: 20-row cycle with custom profile. - for ry in 0..zone.rect.h { - let stripe = (ry + scroll).rem_euclid(20); - let level_idx = match stripe { - 0..=1 => 0, - 2..=4 => 1, - 5..=8 => 2, - 9..=12 => 3, - 13..=15 => 4, - 16..=18 => 3, - _ => 2, - }; - let (ch, i) = levels[level_idx]; - for rx in 0..zone.rect.w { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 3. ParseDump — `xxxx: AB CD EF ...` hex memory dump. -fn paint_parse_dump( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let scroll = base / 4; - - for ry in 0..zh { - let addr = (scroll.wrapping_add(ry as i64) & 0xffff) as u32; - for rx in 0..zw { - let (ch, i) = if rx < 4 { - let nibble = ((addr >> ((3 - rx) * 4)) & 0xf) as usize; - (HEX[nibble], 0.72) - } else if rx == 4 { - (':', 0.55) - } else if rx == 5 { - (' ', 0.10) - } else { - let rel = rx - 6; - let byte_idx = rel / 3; - let pos = rel % 3; - let b = sample(stream, base + (ry as i64) * 9 + byte_idx as i64) as u32; - match pos { - 0 => (HEX[((b >> 4) & 0xf) as usize], 0.92), - 1 => (HEX[(b & 0xf) as usize], 0.92), - _ => (' ', 0.12), - } - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 4. RegisterDump — named registers with hex values. -fn paint_register_dump( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - const NAMES: &[&str] = &[ - "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7", "PC", "SP", "LR", "SR", - ]; - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - - for ry in 0..zh { - let name_cycle = ((base / 8) as usize).wrapping_add(ry as usize) % NAMES.len(); - let name = NAMES[name_cycle]; - let mut row: Vec<(char, f32)> = Vec::with_capacity(zw as usize); - for ch in name.chars() { - row.push((ch, 0.82)); - } - row.push((':', 0.55)); - row.push((' ', 0.10)); - row.push(('0', 0.70)); - row.push(('x', 0.70)); - for i in 0..8 { - let nib = sample(stream, base + (ry as i64) * 5 + i as i64) as u32; - row.push((HEX[(nib & 0xf) as usize], 0.95)); - } - while row.len() < zw as usize { - row.push((' ', 0.10)); - } - for (rx, &(ch, i)) in row.iter().take(zw as usize).enumerate() { - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 5. TextmarkConverter — left side raw stream → `⇒` → right side transformed. -fn paint_textmark( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let mid_y = zh / 2; - let mid_x = zw / 2; - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - - let transform = |c: char| -> char { - if c.is_ascii_alphabetic() { - let b = if c.is_ascii_lowercase() { b'a' } else { b'A' }; - let off = ((c as u8) - b + 13) % 26; - (b + off) as char - } else if c.is_ascii_digit() { - let d = (c as u8) - b'0'; - (b'0' + (9 - d)) as char - } else if c.is_ascii() { - HEX[((c as u8) >> 4 & 0x0f) as usize] - } else { - HEX[((c as u32) & 0x0f) as usize] - } - }; - - for ry in 0..zh { - for rx in 0..zw { - if ry == mid_y && rx == mid_x { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - '⇒', - color, - 1.0, - zone_id, - ); - continue; - } - if ry == mid_y { - let (ch, i) = if rx < mid_x { - let c = sample(stream, base + (mid_x - 1 - rx) as i64); - (c, 0.95) - } else { - let c = sample(stream, base + (rx - mid_x - 1) as i64); - (transform(c), 0.95) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } else { - let d = ((ry - mid_y).abs() as f32) / (zh as f32 * 0.5); - let fade = (0.48 - d * 0.32).max(0.12); - let c = sample(stream, base + (ry as i64) * 7 + rx as i64); - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - c, - color, - fade, - zone_id, - ); - } - } - } -} - -/// 6. Cellular1D — elementary CA, seeded from the stream, evolves top-to-bottom. -fn paint_cellular( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - rule: u8, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let zw = zone.rect.w as usize; - let zh = zone.rect.h as usize; - if zw < 3 || zh < 2 { - paint_fill(grid, zone, zone_id, '·', 0.20); - return; - } - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let t_shift = (zone.pulse * 2.0) as i64; - - // Seed top row from stream bits. - let mut row = vec![false; zw]; - for rx in 0..zw { - let s = sample(stream, base + rx as i64 + t_shift) as u32; - row[rx] = (s & 1) == 1; - } - // Paint row, then evolve. - for ry in 0..zh { - for rx in 0..zw { - let (ch, i) = if row[rx] { ('█', 0.93) } else { ('·', 0.18) }; - put( - grid, - zone.rect.x + rx as i32, - zone.rect.y + ry as i32, - ch, - color, - i, - zone_id, - ); - } - if ry + 1 >= zh { - break; - } - let prev = row.clone(); - for rx in 0..zw { - let l = prev[(rx + zw - 1) % zw]; - let c = prev[rx]; - let r = prev[(rx + 1) % zw]; - let pat = ((l as u8) << 2) | ((c as u8) << 1) | (r as u8); - row[rx] = ((rule >> pat) & 1) == 1; - } - } -} - -/// 8. Marquee — scrolling stream text with bright middle band. -fn paint_marquee( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let base: i64 = (cursor as i64) - (zone.tap_offset as i64) + glitch_offset(zone); - let zw = zone.rect.w; - let zh = zone.rect.h; - let mid_y = zh / 2; - for ry in 0..zh { - for rx in 0..zw { - let ci = cell_index(rx, ry, zw, zh, zone.flow_dir); - let ch = sample(stream, base + ci); - let i = if ry == mid_y { - 1.0 - } else { - let d = ((ry - mid_y).abs() as f32) / (zh as f32 * 0.5); - (0.78 - d * 0.48).max(0.30) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 9. DensityGrid — 2-cell block mosaic at stream-byte density. -fn paint_density_grid( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - stream: &[char], - cursor: f32, -) { - let color = color_for(zone.side); - let base: i64 = (cursor as i64) - (zone.tap_offset as i64); - let levels: &[(char, f32)] = &[ - (' ', 0.08), - ('░', 0.35), - ('▒', 0.58), - ('▓', 0.82), - ('█', 1.00), - ]; - let block_w: i32 = 2; - let blocks_per_row = (zone.rect.w + block_w - 1) / block_w; - for ry in 0..zone.rect.h { - for bx in 0..blocks_per_row { - let rx0 = bx * block_w; - let idx = (ry as i64) * (blocks_per_row as i64) + bx as i64; - let s = sample(stream, base + idx) as u32; - let density = ((s & 0xff) as usize * levels.len()) / 256; - let density = density.min(levels.len() - 1); - let (ch, i) = levels[density]; - for k in 0..block_w { - let rx = rx0 + k; - if rx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } - } -} - -/// 10. AttentionMatrix — sparse transformer-attention pattern: diagonal band, -/// a few sink columns, rare hotspots. Everything else mostly dark. -fn paint_attention(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - let t = zone.pulse; - - // A handful of "sink" columns (attention sinks) that migrate slowly. - let n_sinks = (1 + (zw / 14)).max(2); - let mut sinks: Vec = Vec::with_capacity(n_sinks as usize); - for i in 0..n_sinks { - let phase = (t * 0.2 + i as f32 * 0.7).sin(); - let pos = (((phase + 1.0) * 0.5) * (zw as f32 - 2.0)) as i32 + 1; - sinks.push(pos.clamp(0, zw - 1)); - } - - for ry in 0..zh { - for rx in 0..zw { - let mut score: f32 = 0.12; - - // Diagonal band — attending to self/near tokens. - let diag_x = (ry as f32 / zh.max(1) as f32) * (zw as f32); - let d = (diag_x - rx as f32).abs(); - if d < 2.0 { - score = score.max(0.88 - d * 0.25); - } - - // Sink columns — always some attention. - for &s in &sinks { - let cd = (rx - s).abs(); - if cd == 0 { - score = score.max(0.78); - } else if cd == 1 { - score = score.max(0.42); - } - } - - // Rare random hotspots that shimmer with time. - let h = ihash(rx, ry, (t * 2.0) as i32); - if (h & 0xff) < 4 { - score = score.max(0.95); - } - - let (ch, i) = if score > 0.85 { - ('█', 1.0) - } else if score > 0.60 { - ('▓', 0.80) - } else if score > 0.38 { - ('▒', 0.55) - } else if score > 0.18 { - ('░', 0.32) - } else { - ('.', 0.14) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - } -} - -/// 11. ProbField — top-K token distribution, **conditioned on neighbors**. -/// -/// Each row's probability is derived by sampling the hose at one of this -/// zone's neighbors' current windows. If a zone has no neighbors (isolated, -/// rare), it falls back to its own tap. The ordering by mass is real: the -/// distribution collapses onto a top candidate each frame, with the runners-up -/// visibly competing below it. As the hose advances, the neighbors' views -/// shift, and this zone's entire distribution reshuffles in response — a -/// picture of attention doing what attention does. -fn paint_prob_field(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &PaintCtx) { - const CANDIDATES: &[&str] = &[ - "the", "and", "to", "of", "is", "a", "in", "that", "it", "for", "fn", "::", "0x", "->", - "=>", "if", "fold", "self", "void", "phi", "sigma", "delta", "ROUTINE", "ACK", "MOV", - "yield", "loop", "ok", "recur", "echo", "bind", "map", "tau", "λ", "∇", - ]; - let color = color_for(zone.side); - let zw = zone.rect.w as usize; - let zh = zone.rect.h as usize; - if zw < 14 || zh < 2 { - paint_fill(grid, zone, zone_id, '·', 0.25); - return; - } - - // Collect this zone's neighbors' tap offsets. Fall back to own tap if - // isolated so the formation still reads coherently. - let neighbors = &ctx.adjacency[zone_id as usize]; - let tap_pool: Vec = if neighbors.is_empty() { - vec![zone.tap_offset] - } else { - neighbors - .iter() - .map(|&j| ctx.zones[j as usize].tap_offset) - .collect() - }; - - let top_k = zh.min(16); - let bar_width = zw.saturating_sub(13).max(4); - - // Each row samples from ONE neighbor's current window — the row's weight - // is what that neighbor is "focusing on" right now. Skew-cubed so one or - // two candidates dominate (real LM distributions have heavy peaks). - let mut probs: Vec<(f32, &str)> = Vec::with_capacity(top_k); - let mut sum = 0.0_f32; - for i in 0..top_k { - let tap = tap_pool[i % tap_pool.len()]; - let neighbor_window_offset = (i as i64) * 23 + (ctx.cursor as i64 / 3); - let s = sample( - ctx.stream, - (ctx.cursor as i64) - (tap as i64) + neighbor_window_offset, - ) as u32; - let raw = 0.01 + ((s & 0xff) as f32) / 255.0; - let weight = raw.powi(3); - sum += weight; - let name = CANDIDATES[(s as usize >> 4) % CANDIDATES.len()]; - probs.push((weight, name)); - } - for p in &mut probs { - p.0 /= sum.max(1e-6); - } - probs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); - - paint_fill(grid, zone, zone_id, ' ', 0.08); - - for (row, (p, name)) in probs.iter().enumerate().take(zh) { - let ry = row as i32; - let fill = ((*p * bar_width as f32).round() as usize).min(bar_width); - for rx in 0..(bar_width as i32) { - let (ch, i) = if (rx as usize) < fill { - ('█', (0.55 + p * 0.45).min(1.0)) - } else { - ('░', 0.20) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - i, - zone_id, - ); - } - let p_str = format!(" {:.2}", p.min(0.99)); - let mut cx = bar_width as i32 + 1; - for ch in p_str.chars() { - if cx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + cx, - zone.rect.y + ry, - ch, - color, - 0.82, - zone_id, - ); - cx += 1; - } - cx += 1; - for ch in name.chars() { - if cx >= zone.rect.w { - break; - } - put( - grid, - zone.rect.x + cx, - zone.rect.y + ry, - ch, - color, - 0.95, - zone_id, - ); - cx += 1; - } - } -} - -/// 12. ImagePanel — streams a pre-rendered braille image into the zone with -/// glitching effects. Source: dotmax's full ImageRenderer pipeline -/// (Floyd-Steinberg → Otsu → braille mapping). Each frame, a few rows get -/// scanline-torn, a sprinkle of cells get block-char corrupted, and bursts -/// of stream chars bleed through as noise. -// ─────────────── abstract dither-phase system ─────────────── -// -// Instead of a smooth per-cell wave, the composition is in one of two -// macro-phases at any moment: -// -// • Stable — ~11 real-seconds of a SINGLE dither variant everywhere -// • Sweep — ~1.6 seconds where a geometric sweep-front cuts across -// every image panel, carving the old variant away and -// crystallizing the new one behind it. A bright heavy-block -// highlight marks the sweep front at all times. -// -// All image panels share the same phase/timing — synchronized, deliberate, -// momentous. The sweep direction rotates per cycle (horizontal, vertical, -// diagonal, anti-diagonal, radial). - -#[derive(Clone, Copy)] -enum SweepKind { - Horizontal, - Vertical, - Diagonal, - AntiDiag, - Radial, -} - -#[derive(Clone, Copy)] -enum DitherPhase { - Stable { - idx: usize, - }, - Sweep { - from: usize, - to: usize, - t: f32, - kind: SweepKind, - }, -} - -const DITHER_NAMES: &[&str] = &["NONE", "FLOYD", "BAYER", "ATKIN"]; - -/// Convert the global cursor into a dither phase, with a per-zone offset -/// (in seconds) so different image panels run on their own clocks. -/// Asynchronous — each panel transitions when ITS clock says so. -fn dither_phase(cursor: f32, n_variants: usize, offset_secs: f32) -> DitherPhase { - const PERIOD: f32 = 11.0; - const TRANSIT: f32 = 1.6; - let cycle = PERIOD + TRANSIT; - let secs = (cursor / 48.0) + offset_secs; - let cycle_num = secs.div_euclid(cycle) as i64; - let in_cycle = secs.rem_euclid(cycle); - - let kind = match (cycle_num.rem_euclid(5)) as usize { - 0 => SweepKind::Horizontal, - 1 => SweepKind::Vertical, - 2 => SweepKind::Diagonal, - 3 => SweepKind::AntiDiag, - _ => SweepKind::Radial, - }; - - if in_cycle < PERIOD { - DitherPhase::Stable { - idx: (cycle_num.rem_euclid(n_variants as i64)) as usize, - } - } else { - let raw = ((in_cycle - PERIOD) / TRANSIT).clamp(0.0, 1.0); - // Smoothstep — dramatic ease-in/out rather than linear. - let t = raw * raw * (3.0 - 2.0 * raw); - let from = cycle_num.rem_euclid(n_variants as i64) as usize; - let to = (cycle_num + 1).rem_euclid(n_variants as i64) as usize; - DitherPhase::Sweep { from, to, t, kind } - } -} - -/// Sacred glyph cycled per sweep kind — the symbol that marks the moment of -/// transition. Each geometric sweep wears its own sign. -#[inline] -fn sweep_front_glyph(kind: SweepKind) -> char { - match kind { - SweepKind::Horizontal => '✦', // four-pointed star - SweepKind::Vertical => '✧', // outlined star - SweepKind::Diagonal => '◉', // circled dot - SweepKind::AntiDiag => '☸', // wheel of dharma - SweepKind::Radial => '⚘', // flower - } -} - -/// Progress at cell (rx, ry) along the sweep direction, ∈ [0, 1]. -#[inline] -fn sweep_progress(rx: i32, ry: i32, zw: i32, zh: i32, kind: SweepKind) -> f32 { - let zwf = zw.max(1) as f32; - let zhf = zh.max(1) as f32; - match kind { - SweepKind::Horizontal => rx as f32 / zwf, - SweepKind::Vertical => ry as f32 / zhf, - SweepKind::Diagonal => (rx as f32 + (ry as f32) * 2.0) / (zwf + zhf * 2.0), - SweepKind::AntiDiag => ((zwf - rx as f32 - 1.0) + (ry as f32) * 2.0) / (zwf + zhf * 2.0), - SweepKind::Radial => { - let cx = zwf * 0.5; - let cy = zhf * 0.5; - let dx = rx as f32 - cx; - let dy = (ry as f32 - cy) * 2.0; - let d = (dx * dx + dy * dy).sqrt(); - let max_d = ((cx * cx) + (cy * 2.0).powi(2)).sqrt().max(0.01); - d / max_d - } - } -} - -fn paint_image_panel( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - asset_idx: usize, - ctx: &PaintCtx, -) { - let color = color_for(zone.side); - let zw = zone.rect.w; - let zh = zone.rect.h; - if ctx.assets.is_empty() || asset_idx >= ctx.assets.len() { - paint_fill(grid, zone, zone_id, '·', 0.3); - return; - } - let asset = &ctx.assets[asset_idx]; - if asset.variants.is_empty() { - paint_fill(grid, zone, zone_id, '·', 0.3); - return; - } - let n_variants = asset.variants.len(); - // Per-zone offset (seconds) — each panel runs on its own dither clock. - // tap_offset is a stable per-zone integer; modulate it to seconds. - let zone_offset = (zone.tap_offset as f32 * 0.0019) + zone.pulse * 0.7; - let phase = dither_phase(ctx.cursor, n_variants, zone_offset); - - // Glitch modulation from pulse (still there for texture, not dither). - let pulse_i = (zone.pulse * 3.7) as i32; - let tear_rows: [i32; 3] = [ - ((zone.pulse * 4.2).sin() * zh as f32) as i32 % zh.max(1), - ((zone.pulse * 1.9 + 1.3).sin() * zh as f32) as i32 % zh.max(1), - ((zone.pulse * 2.6 + 3.1).sin() * zh as f32) as i32 % zh.max(1), - ]; - let tear_amts: [i32; 3] = [ - ((zone.pulse * 5.3).sin() * 6.0) as i32, - ((zone.pulse * 3.8 + 0.7).sin() * 4.0) as i32, - ((zone.pulse * 2.1 + 2.4).sin() * 8.0) as i32, - ]; - - let base_stream: i64 = (ctx.cursor as i64) - (zone.tap_offset as i64); - - // Width of the bright sweep front as a fraction of the total sweep distance. - const FRONT_BAND: f32 = 0.035; - - // Native-resolution scrolling: image is sampled 1:1 from braille cells - // and wraps modularly. The frame stays fixed; the image scrolls inside - // it. No warping — aspect ratio preserved. - let scroll_x = (zone.pulse * 1.7) as i32; - let scroll_y = (zone.pulse * 0.9) as i32; - - for ry in 0..zh { - let mut tear_dx = 0_i32; - for k in 0..3 { - if ry == tear_rows[k] { - tear_dx = tear_amts[k]; - } - } - - for rx in 0..zw { - // Decide which variant owns this cell, + whether this cell is - // currently ON the sweep front (gets a bright highlight). - let (v_idx, on_front, front_kind) = match phase { - DitherPhase::Stable { idx } => (idx, false, SweepKind::Horizontal), - DitherPhase::Sweep { from, to, t, kind } => { - let p = sweep_progress(rx, ry, zw, zh, kind); - let front = (p - t).abs() < FRONT_BAND; - let idx = if p < t { to } else { from }; - (idx, front, kind) - } - }; - let variant = &asset.variants[v_idx.min(n_variants - 1)]; - - let vw = variant.w as i32; - let vh = variant.h as i32; - let srx = (rx + tear_dx + scroll_x).rem_euclid(vw.max(1)) as usize; - let sry = (ry + scroll_y).rem_euclid(vh.max(1)) as usize; - let img_ch = variant - .cells - .get(sry) - .and_then(|row| row.get(srx)) - .copied() - .unwrap_or(' '); - - let h = ihash(rx, ry, pulse_i); - let (ch, intensity, fg_override) = if on_front { - // Sweep front — sacred glyph marks the moment of transition. - (sweep_front_glyph(front_kind), 1.0, Some((255, 50, 50))) - } else if h & 0x7f == 0 { - let blocks: &[char] = &['█', '▓', '▒', '░']; - (blocks[(h as usize >> 7) % blocks.len()], 1.0, None) - } else if h & 0x3f == 0 { - let sc = sample(ctx.stream, base_stream + (ry as i64) * 7 + rx as i64); - (sc, 0.85, None) - } else if img_ch == '\u{2800}' { - (' ', 0.08, None) - } else { - (img_ch, 0.92, None) - }; - let fg = fg_override.unwrap_or(color); - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - fg, - intensity, - zone_id, - ); - } - } - - // Top-left: asset name. - let label = asset.name; - for (i, ch) in label.chars().enumerate() { - let lx = zone.rect.x + 1 + i as i32; - if i as i32 + 1 < zw { - put(grid, lx, zone.rect.y, ch, (255, 50, 50), 1.0, zone_id); - } - } - // Bottom-right: dither phase tag — calculated readout of state. - let tag: String = match phase { - DitherPhase::Stable { idx } => { - format!("[{}]", DITHER_NAMES[idx.min(DITHER_NAMES.len() - 1)]) - } - DitherPhase::Sweep { from, to, .. } => format!( - "{}→{}", - DITHER_NAMES[from.min(DITHER_NAMES.len() - 1)], - DITHER_NAMES[to.min(DITHER_NAMES.len() - 1)], - ), - }; - let tag_y = zone.rect.y + zh - 1; - let tag_x_start = zone.rect.x + zw - (tag.chars().count() as i32) - 1; - for (i, ch) in tag.chars().enumerate() { - let lx = tag_x_start + i as i32; - if lx >= zone.rect.x && lx < zone.rect.x + zw { - put(grid, lx, tag_y, ch, (255, 50, 50), 1.0, zone_id); - } - } -} - -/// 13. RaytraceCube — wireframe cube on BLACK background. Built from 8 -/// corners + 12 edges, rotated via raytracer's yaw/pitch helper. Lines -/// rasterized with Bresenham. Zero mask, zero pipes — pure geometry in -/// the middle of the chaos. -fn paint_raytrace_cube(grid: &mut [Vec], zone: &Zone, zone_id: u16) { - let zw = zone.rect.w; - let zh = zone.rect.h; - if zw < 6 || zh < 4 { - paint_fill(grid, zone, zone_id, ' ', 0.05); - return; - } - - // Fill zone with true black bg — claim ownership at low positive intensity. - for ry in 0..zh { - for rx in 0..zw { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ' ', - (0, 0, 0), - 0.02, - zone_id, - ); - } - } - - // 8 corners of a unit cube centered at origin. - let corners = [ - Vector3::new(-1.0, -1.0, -1.0), - Vector3::new(1.0, -1.0, -1.0), - Vector3::new(1.0, 1.0, -1.0), - Vector3::new(-1.0, 1.0, -1.0), - Vector3::new(-1.0, -1.0, 1.0), - Vector3::new(1.0, -1.0, 1.0), - Vector3::new(1.0, 1.0, 1.0), - Vector3::new(-1.0, 1.0, 1.0), - ]; - let edges: [(usize, usize); 12] = [ - (0, 1), - (1, 2), - (2, 3), - (3, 0), // back face - (4, 5), - (5, 6), - (6, 7), - (7, 4), // front face - (0, 4), - (1, 5), - (2, 6), - (3, 7), // connecting edges - ]; - - let yaw = zone.pulse * 0.55; - let pitch = (zone.pulse * 0.37).sin() * 0.45; - let roll = (zone.pulse * 0.22).cos() * 0.25; - - // Project each rotated corner to zone cell coords. - let zwf = zw as f32; - let zhf = zh as f32; - let cam_z = 3.2_f32; - let viewport_w = 2.8_f32; - let viewport_h = 2.8_f32; - - let projected: [Option<(i32, i32)>; 8] = { - let mut out = [None; 8]; - for (i, &c) in corners.iter().enumerate() { - let r = rotate_vec_yaw_pitch_roll(c, yaw, pitch, roll); - let z = r.z - cam_z; - if z >= -0.01 { - continue; - } // behind camera - let px = r.x * (-1.0 / z) / (viewport_w * 0.5); - let py = r.y * (-1.0 / z) / (viewport_h * 0.5); - let cx = (px + 1.0) * 0.5 * zwf; - let cy = (1.0 - (py + 1.0) * 0.5) * zhf * 2.0; // aspect correction - let cy = cy * 0.5; - out[i] = Some((cx.round() as i32, cy.round() as i32)); - } - out - }; - - // Draw edges via Bresenham. Back edges (those with any corner having - // a more negative rotated z) rendered with thinner chars — cheap hidden-line hint. - for &(a, b) in &edges { - if let (Some((x0, y0)), Some((x1, y1))) = (projected[a], projected[b]) { - let dx = (x1 - x0).abs(); - let dy = -(y1 - y0).abs(); - let sx = if x0 < x1 { 1 } else { -1 }; - let sy = if y0 < y1 { 1 } else { -1 }; - let mut err = dx + dy; - let (mut x, mut y) = (x0, y0); - loop { - if x >= 0 && x < zw && y >= 0 && y < zh { - put( - grid, - zone.rect.x + x, - zone.rect.y + y, - '█', - (220, 35, 35), - 1.0, - zone_id, - ); - } - if x == x1 && y == y1 { - break; - } - let e2 = 2 * err; - if e2 >= dy { - err += dy; - x += sx; - } - if e2 <= dx { - err += dx; - y += sy; - } - } - } - } -} - -// ─────────────── betting UI formations ─────────────── - -fn paint_box_border(grid: &mut [Vec], zone: &Zone, zone_id: u16, intensity: f32) { - let zw = zone.rect.w; - let zh = zone.rect.h; - if zw < 2 || zh < 2 { - return; - } - for x in 1..zw - 1 { - put( - grid, - zone.rect.x + x, - zone.rect.y, - '─', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + x, - zone.rect.y + zh - 1, - '─', - UI_WHITE, - intensity, - zone_id, - ); - } - for y in 1..zh - 1 { - put( - grid, - zone.rect.x, - zone.rect.y + y, - '│', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + y, - '│', - UI_WHITE, - intensity, - zone_id, - ); - } - put( - grid, - zone.rect.x, - zone.rect.y, - '┌', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y, - '┐', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x, - zone.rect.y + zh - 1, - '└', - UI_WHITE, - intensity, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + zh - 1, - '┘', - UI_WHITE, - intensity, - zone_id, - ); -} - -fn put_str( - grid: &mut [Vec], - x: i32, - y: i32, - s: &str, - color: (u8, u8, u8), - i: f32, - oid: u16, - max_x: i32, -) { - let mut cx = x; - for ch in s.chars() { - if cx >= max_x { - break; - } - put(grid, cx, y, ch, color, i, oid); - cx += 1; - } -} - -/// Atm — top-corner balance display. -fn paint_atm(grid: &mut [Vec], zone: &Zone, zone_id: u16, balance: u32, cursor: f32) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 1.0); - let ix = zone.rect.x + 2; - let max_x = zone.rect.x + zone.rect.w - 1; - put_str( - grid, - ix, - zone.rect.y + 1, - "ATM ::", - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - let bal = format!("$ {:08}", balance); - put_str( - grid, - ix, - zone.rect.y + 2, - &bal, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - // Tiny cursor indicator - let blink = ((cursor * 0.5) as i32) % 2 == 0; - if blink && zone.rect.h >= 4 { - put_str( - grid, - ix, - zone.rect.y + 3, - "● ONLINE", - (255, 90, 90), - 1.0, - zone_id, - max_x, - ); - } else if zone.rect.h >= 4 { - put_str( - grid, - ix, - zone.rect.y + 3, - "○ ONLINE", - (200, 60, 60), - 0.9, - zone_id, - max_x, - ); - } -} - -/// AgentSlot — labeled "ENTER AGENT" panel for a single player. -fn paint_agent_slot( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - player: u8, - scene_input: &str, -) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 0.9); - let ix = zone.rect.x + 2; - let max_x = zone.rect.x + zone.rect.w - 1; - let label = if player == 0 { "AGENT P1" } else { "AGENT P2" }; - put_str( - grid, - ix, - zone.rect.y + 1, - label, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - let prompt = "[ENTER AGENT >]"; - put_str( - grid, - ix, - zone.rect.y + 2, - prompt, - (255, 110, 110), - 1.0, - zone_id, - max_x, - ); - // Show typed input under P1 only (one buffer for the demo). - if player == 0 && !scene_input.is_empty() && zone.rect.h >= 4 { - let truncated: String = scene_input - .chars() - .take((zone.rect.w - 4) as usize) - .collect(); - put_str( - grid, - ix, - zone.rect.y + 3, - &truncated, - UI_WHITE, - 1.0, - zone_id, - max_x, - ); - } -} - -/// Live chess board — converts shakmaty position to braille via dotmax::chess. -/// During global dither sweeps, a sacred sweep-front cuts across the board so -/// the chess "dithers into" the center in lockstep with the image panels. -fn paint_chess_board( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - pos: &Chess, - cursor: f32, -) { - paint_fill(grid, zone, zone_id, ' ', 0.04); - let opts = RenderOptions { - target_width: Some(zone.rect.w as usize), - target_height: Some(zone.rect.h as usize), - ..Default::default() - }; - if let Ok(braille_grid) = render_position_with_options(pos, &opts) { - let (gw, gh) = braille_grid.dimensions(); - // Determine sweep state for the dither overlay. - let phase = dither_phase(cursor, 4, 0.0); - for ry in 0..zone.rect.h.min(gh as i32) { - for rx in 0..zone.rect.w.min(gw as i32) { - let ch = braille_grid.get_char(rx as usize, ry as usize); - let (color, intensity) = if ch == '\u{2800}' || ch == ' ' { - ((50, 5, 5), 0.30) - } else { - (UI_WHITE, 1.0) - }; - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - ch, - color, - intensity, - zone_id, - ); - } - } - // Chess "dithers in" — sweep front overlay during transitions. - if let DitherPhase::Sweep { t, kind, .. } = phase { - let zw = zone.rect.w; - let zh = zone.rect.h; - for ry in 0..zh { - for rx in 0..zw { - let p = sweep_progress(rx, ry, zw, zh, kind); - if (p - t).abs() < 0.04 { - put( - grid, - zone.rect.x + rx, - zone.rect.y + ry, - sweep_front_glyph(kind), - UI_WHITE, - 1.0, - zone_id, - ); - } - } - } - } - } -} - -/// Big "[ CASH OUT ]" payout button. Solid block frame, white centered text. -fn paint_payout_button(grid: &mut [Vec], zone: &Zone, zone_id: u16, cursor: f32) { - let zw = zone.rect.w; - let zh = zone.rect.h; - paint_fill(grid, zone, zone_id, ' ', 0.0); - // Solid block border - for x in 0..zw { - put( - grid, - zone.rect.x + x, - zone.rect.y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - put( - grid, - zone.rect.x + x, - zone.rect.y + zh - 1, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - } - for y in 0..zh { - put( - grid, - zone.rect.x, - zone.rect.y + y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - put( - grid, - zone.rect.x + zw - 1, - zone.rect.y + y, - '█', - UI_WHITE, - 1.0, - zone_id, - ); - } - // Pulsing label - let text = "[ CASH OUT ]"; - let len = text.chars().count() as i32; - let mid_y = zone.rect.y + zh / 2; - let mid_x = zone.rect.x + (zw - len) / 2; - let pulse = ((cursor * 0.05).sin() + 1.0) * 0.5; // 0..1 - let color = ( - 255, - (60.0 + pulse * 60.0) as u8, - (60.0 + pulse * 60.0) as u8, - ); - put_str( - grid, - mid_x, - mid_y, - text, - color, - 1.0, - zone_id, - zone.rect.x + zw - 1, - ); -} - -/// Live terminal input — prompt + buffer + blinking cursor. -fn paint_terminal_input( - grid: &mut [Vec], - zone: &Zone, - zone_id: u16, - buffer: &str, - cursor: f32, -) { - paint_fill(grid, zone, zone_id, ' ', 0.05); - paint_box_border(grid, zone, zone_id, 0.9); - let ix = zone.rect.x + 2; - let mid_y = zone.rect.y + zone.rect.h / 2; - let max_x = zone.rect.x + zone.rect.w - 1; - let prompt = "INPUT> "; - put_str(grid, ix, mid_y, prompt, UI_WHITE, 1.0, zone_id, max_x); - let mut bx = ix + prompt.chars().count() as i32; - for ch in buffer.chars() { - if bx >= max_x - 1 { - break; - } - put(grid, bx, mid_y, ch, UI_WHITE, 1.0, zone_id); - bx += 1; - } - // Blinking cursor block - let blink = ((cursor / 24.0) as i32) % 2 == 0; - if blink && bx < max_x { - put(grid, bx, mid_y, '█', UI_WHITE, 1.0, zone_id); - } -} - -// ─────────────── formation dispatch ─────────────── - -fn paint_formation(grid: &mut [Vec], zone: &Zone, zone_id: u16, ctx: &PaintCtx) { - match zone.formation { - Formation::Raytrace => paint_raytrace(grid, zone, zone_id), - Formation::BlockStrata => paint_block_strata(grid, zone, zone_id), - Formation::ParseDump => paint_parse_dump(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::RegisterDump => paint_register_dump(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::TextmarkConverter => paint_textmark(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::Cellular1D { rule } => { - paint_cellular(grid, zone, zone_id, rule, ctx.stream, ctx.cursor) - } - Formation::Marquee => paint_marquee(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::DensityGrid => paint_density_grid(grid, zone, zone_id, ctx.stream, ctx.cursor), - Formation::AttentionMatrix => paint_attention(grid, zone, zone_id), - Formation::ProbField => paint_prob_field(grid, zone, zone_id, ctx), - Formation::ImagePanel { asset } => paint_image_panel(grid, zone, zone_id, asset, ctx), - Formation::RaytraceCube => paint_raytrace_cube(grid, zone, zone_id), - Formation::Atm => paint_atm(grid, zone, zone_id, ctx.balance, ctx.cursor), - Formation::AgentSlot { player } => { - paint_agent_slot(grid, zone, zone_id, player, ctx.input_buffer) - } - Formation::ChessBoard => paint_chess_board(grid, zone, zone_id, ctx.chess_pos, ctx.cursor), - Formation::PayoutButton => paint_payout_button(grid, zone, zone_id, ctx.cursor), - Formation::TerminalInput => { - paint_terminal_input(grid, zone, zone_id, ctx.input_buffer, ctx.cursor) - } - } -} - -/// Compression operator applied by a pipe as chars flow through it. -#[derive(Clone, Copy)] -enum Transform { - /// XOR each byte with the key. - Xor(u8), - /// ROT-N on ASCII letters (N = signed shift). - Rot(i8), - /// Encode low nibble as a hex digit. - HexEncode, - /// Reverse byte bits. - BitRev, - /// Keep every other char, drop the rest to '|'. - Stripe, -} - -fn pick_transform() -> Transform { - match r_u32() % 5 { - 0 => Transform::Xor(0x33 + ((r_u32() as u8) & 0x7F)), - 1 => Transform::Rot((1 + (r_u32() % 25)) as i8), - 2 => Transform::HexEncode, - 3 => Transform::BitRev, - _ => Transform::Stripe, - } -} - -fn apply_transform(c: char, t: Transform) -> char { - match t { - Transform::Xor(k) => { - if c.is_ascii() { - let b = (c as u8) ^ k; - if (b as char).is_ascii_graphic() { - b as char - } else { - HEX[(b & 0x0f) as usize] - } - } else { - HEX[((c as u32) & 0x0f) as usize] - } - } - Transform::Rot(n) => { - if c.is_ascii_alphabetic() { - let base = if c.is_ascii_uppercase() { b'A' } else { b'a' }; - let shifted = (((c as u8 - base) as i16 + n as i16).rem_euclid(26)) as u8; - (base + shifted) as char - } else { - c - } - } - Transform::HexEncode => HEX[((c as u32) & 0x0f) as usize], - Transform::BitRev => { - if c.is_ascii() { - let mut b = c as u8; - b = (b >> 4) | (b << 4); - b = ((b >> 2) & 0x33) | ((b << 2) & 0xcc); - b = ((b >> 1) & 0x55) | ((b << 1) & 0xaa); - if (b as char).is_ascii_graphic() { - b as char - } else { - HEX[(b & 0x0f) as usize] - } - } else { - HEX[((c as u32) & 0x0f) as usize] - } - } - Transform::Stripe => { - if (c as u32) & 1 == 0 { - '|' - } else { - c - } - } - } -} - -/// Operator zone symbols — drawn in the middle of every pipe to identify -/// the compression happening inline. Variable-length: 2 or 3 chars. -fn transform_symbols(t: Transform) -> [char; 3] { - match t { - Transform::Xor(k) => ['⊕', HEX[((k >> 4) & 0xf) as usize], HEX[(k & 0xf) as usize]], - Transform::Rot(n) => { - let mag = (n.unsigned_abs() as usize) % 26; - ['↻', HEX[(mag / 16) as usize], HEX[(mag % 16) as usize]] - } - Transform::HexEncode => ['#', '1', '6'], - Transform::BitRev => ['⊥', '↔', '⊥'], - Transform::Stripe => ['▮', '|', '▮'], - } -} - -/// Pipe: an active conduit between two zones. Cells run from inside the source -/// zone, across the shared border, into the destination zone. Along the pipe, -/// chars pass through three zones of painting: -/// [INPUT: raw source chars] → [OPERATOR: transform symbol] → [OUTPUT: transformed] -/// The whole composition becomes a compression machine — each pipe a stage. -struct Pipe { - from: u16, - to: u16, - cells: Vec<(i32, i32)>, // ordered source-end → dest-end - transform: Transform, - step: i64, // stream-chars between consecutive pipe cells (1..=4) -} - -/// Try to build a pipe between two adjacent zones. Returns None if they -/// aren't adjacent or the shared edge is too short to carry a useful pipe. -fn try_build_pipe(zones: &[Zone], i: usize, j: usize) -> Option { - let a = zones[i].base_rect; - let b = zones[j].base_rect; - let min_edge = 4; - let len_each = 5; // cells extending into each zone from the shared border - - // Helper to finish the Pipe once `cells` are built - let mk = |from: u16, to: u16, cells: Vec<(i32, i32)>| -> Pipe { - Pipe { - from, - to, - cells, - transform: pick_transform(), - step: 1 + (r_u32() % 4) as i64, // per-pipe flow granularity - } - }; - - // A-right touches B-left (flow rightwards: from A into B) - if a.x + a.w == b.x { - let y0 = a.y.max(b.y); - let y1 = (a.y + a.h).min(b.y + b.h); - if y1 - y0 < min_edge { - return None; - } - let y = y0 + (y1 - y0) / 2; - let l_a = len_each.min(a.w - 1).max(2); - let l_b = len_each.min(b.w - 1).max(2); - let cells: Vec<(i32, i32)> = ((a.x + a.w - l_a)..(b.x + l_b)).map(|x| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(i as u16, j as u16, cells)); - } - } - // B-right touches A-left (flow rightwards: from B into A) - if b.x + b.w == a.x { - let y0 = a.y.max(b.y); - let y1 = (a.y + a.h).min(b.y + b.h); - if y1 - y0 < min_edge { - return None; - } - let y = y0 + (y1 - y0) / 2; - let l_a = len_each.min(a.w - 1).max(2); - let l_b = len_each.min(b.w - 1).max(2); - let cells: Vec<(i32, i32)> = ((b.x + b.w - l_b)..(a.x + l_a)).map(|x| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(j as u16, i as u16, cells)); - } - } - // A-bottom touches B-top (flow downwards: from A into B) - if a.y + a.h == b.y { - let x0 = a.x.max(b.x); - let x1 = (a.x + a.w).min(b.x + b.w); - if x1 - x0 < min_edge { - return None; - } - let x = x0 + (x1 - x0) / 2; - let l_a = 4.min(a.h - 1).max(2); - let l_b = 4.min(b.h - 1).max(2); - let cells: Vec<(i32, i32)> = ((a.y + a.h - l_a)..(b.y + l_b)).map(|y| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(i as u16, j as u16, cells)); - } - } - // B-bottom touches A-top (flow downwards: from B into A) - if b.y + b.h == a.y { - let x0 = a.x.max(b.x); - let x1 = (a.x + a.w).min(b.x + b.w); - if x1 - x0 < min_edge { - return None; - } - let x = x0 + (x1 - x0) / 2; - let l_a = 4.min(a.h - 1).max(2); - let l_b = 4.min(b.h - 1).max(2); - let cells: Vec<(i32, i32)> = ((b.y + b.h - l_b)..(a.y + l_a)).map(|y| (x, y)).collect(); - if cells.len() >= 6 { - return Some(mk(j as u16, i as u16, cells)); - } - } - None -} - -fn build_pipes(zones: &[Zone]) -> Vec { - // Build a pipe between EVERY adjacent (non-Nested) pair that admits one. - // The whole screen becomes visibly networked. - let mut pipes = Vec::new(); - for i in 0..zones.len() { - if zones[i].side == Side::Nested { - continue; - } - for j in (i + 1)..zones.len() { - if zones[j].side == Side::Nested { - continue; - } - if let Some(p) = try_build_pipe(zones, i, j) { - pipes.push(p); - } - } - } - pipes -} - -/// Streamer axis — direction of a persistent overlay flow line. -#[derive(Clone, Copy)] -enum StreamerAxis { - Horizontal, - Vertical, - DiagPos, - DiagNeg, -} - -/// A single persistent overlay flow line. -struct Streamer { - axis: StreamerAxis, - anchor: i32, // y for H, x for V, intercept for diagonals (top edge) - speed: f32, // chars / sec - direction: i32, // +1 / -1 — flow direction along the line -} - -fn streamer_cells(axis: StreamerAxis, anchor: i32, w: i32, h: i32) -> Vec<(i32, i32)> { - match axis { - StreamerAxis::Horizontal => (0..w).map(|x| (x, anchor)).collect(), - StreamerAxis::Vertical => (0..h).map(|y| (anchor, y)).collect(), - StreamerAxis::DiagPos => { - let mut out = Vec::new(); - let mut x = anchor; - let mut y = 0; - while y < h { - if x >= 0 && x < w { - out.push((x, y)); - } - y += 1; - x += 2; // step 2 cells horizontally per row → ~45° on screen aspect - } - out - } - StreamerAxis::DiagNeg => { - let mut out = Vec::new(); - let mut x = anchor; - let mut y = 0; - while y < h { - if x >= 0 && x < w { - out.push((x, y)); - } - y += 1; - x -= 2; - } - out - } - } -} - -#[inline] -fn rect_contains(r: Rect, x: i32, y: i32) -> bool { - x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h -} - -fn paint_streamer( - grid: &mut [Vec], - s: &Streamer, - w: i32, - h: i32, - protected: &[Rect], - stream: &[char], - cursor: f32, -) { - let cells = streamer_cells(s.axis, s.anchor, w, h); - let scroll = (cursor * s.speed) as i64; - for (i, &(x, y)) in cells.iter().enumerate() { - if protected.iter().any(|r| rect_contains(*r, x, y)) { - continue; - } - let pos = scroll + (i as i64) * s.direction as i64; - let ch = sample(stream, pos); - put_force(grid, x, y, ch, (250, 55, 55), 0.95); - } -} - -/// Edge-anchored noise injection. Emits a short trail of chars from an edge -/// point inward, scrolling with its own speed. Different from the global hose. -struct NoiseFeed { - pos: (i32, i32), // edge cell - dir: (i32, i32), // (dx, dy) inward unit vector - length: i32, // trail length in cells - seed: u32, // unique noise seed per feed - speed: f32, // chars / sec -} - -const NOISE_POOL: &[char] = &[ - '#', '@', '%', '$', '*', '!', '?', '&', '+', '=', '~', '^', '\\', -]; - -fn paint_noise_feed(grid: &mut [Vec], f: &NoiseFeed, protected: &[Rect], cursor: f32) { - let scroll = (cursor * f.speed) as u32; - for i in 0..f.length { - let x = f.pos.0 + f.dir.0 * i; - let y = f.pos.1 + f.dir.1 * i; - if protected.iter().any(|r| rect_contains(*r, x, y)) { - continue; - } - let h = f - .seed - .wrapping_mul(2_654_435_761) - .wrapping_add(scroll.wrapping_mul(31)) - .wrapping_add(i as u32); - let ch = NOISE_POOL[(h as usize) % NOISE_POOL.len()]; - let trail_fade = 1.0 - (i as f32) / (f.length.max(1) as f32); - let intensity = 0.55 + trail_fade * 0.40; - put_force(grid, x, y, ch, (245, 50, 50), intensity); - } -} - -/// A wandering "dither worm" — a moving point that leaves a fading trail of -/// block-density chars (█▓▒░) across the screen. Ambulates through whatever's -/// there, painting density on top. Bounces off edges, slowly turns at random. -struct DitherFlow { - pos_x: f32, - pos_y: f32, - vel_x: f32, - vel_y: f32, - trail: Vec<(i32, i32)>, - trail_max: usize, -} - -const FLOW_RAMP: &[char] = &['░', '▒', '▓', '█']; - -fn tick_flow(flow: &mut DitherFlow, dt: f32, w: i32, h: i32) { - flow.pos_x += flow.vel_x * dt; - flow.pos_y += flow.vel_y * dt; - if flow.pos_x < 0.0 { - flow.pos_x = 0.0; - flow.vel_x = flow.vel_x.abs(); - } else if flow.pos_x >= w as f32 { - flow.pos_x = (w - 1) as f32; - flow.vel_x = -flow.vel_x.abs(); - } - if flow.pos_y < 1.0 { - flow.pos_y = 1.0; - flow.vel_y = flow.vel_y.abs(); - } else if flow.pos_y >= h as f32 { - flow.pos_y = (h - 1) as f32; - flow.vel_y = -flow.vel_y.abs(); - } - // Slow random wander — rotate velocity vector by a small angle. - if r_f32() < 0.07 { - let theta = (r_f32() - 0.5) * 0.6; - let (ct, st) = (theta.cos(), theta.sin()); - let nvx = flow.vel_x * ct - flow.vel_y * st; - let nvy = flow.vel_x * st + flow.vel_y * ct; - flow.vel_x = nvx; - flow.vel_y = nvy; - } - let cell = (flow.pos_x as i32, flow.pos_y as i32); - if flow.trail.last() != Some(&cell) { - flow.trail.push(cell); - if flow.trail.len() > flow.trail_max { - flow.trail.remove(0); - } - } -} - -fn paint_flow(grid: &mut [Vec], flow: &DitherFlow) { - let n = flow.trail.len(); - if n == 0 { - return; - } - for (i, &(x, y)) in flow.trail.iter().enumerate() { - // 0 = oldest (dimmest, lightest density char) → n-1 = head (brightest, █). - let age_pct = i as f32 / n as f32; - let level = ((age_pct * FLOW_RAMP.len() as f32) as usize).min(FLOW_RAMP.len() - 1); - let intensity = 0.45 + age_pct * 0.55; - put_force(grid, x, y, FLOW_RAMP[level], (255, 70, 70), intensity); - } -} - -/// A short-lived chunk of an image (or the whole thing) blasted onto the -/// screen at a random rect, force-painted over everything. Lives ~0.5–3 sec. -struct GlitchInsertion { - asset_idx: usize, - rect: Rect, - /// Optional sub-rectangle of the asset to sample from. None = full asset. - crop: Option, - spawn_cursor: f32, - duration_chars: f32, - /// Which dither variant to render. Doesn't have to match anything else. - variant_idx: usize, -} - -fn paint_glitch_insertion( - grid: &mut [Vec], - ins: &GlitchInsertion, - assets: &[ImageAsset], - cursor: f32, -) { - if ins.asset_idx >= assets.len() { - return; - } - let asset = &assets[ins.asset_idx]; - if asset.variants.is_empty() { - return; - } - let variant = &asset.variants[ins.variant_idx.min(asset.variants.len() - 1)]; - - let vw = variant.w as i32; - let vh = variant.h as i32; - // crop.x/.y is the source offset where this insertion's (0,0) maps to. - // Native 1:1 sampling — image scrolls/crops, never warps. - let off_x = ins.crop.map(|c| c.x).unwrap_or(0); - let off_y = ins.crop.map(|c| c.y).unwrap_or(0); - - let age = (cursor - ins.spawn_cursor) / ins.duration_chars.max(0.01); - let life_factor = if age < 0.15 { - age / 0.15 - } else if age > 0.85 { - ((1.0 - age) / 0.15).max(0.0) - } else { - 1.0 - }; - - for ry in 0..ins.rect.h { - for rx in 0..ins.rect.w { - let srx = (off_x + rx).rem_euclid(vw.max(1)) as usize; - let sry = (off_y + ry).rem_euclid(vh.max(1)) as usize; - let ch = variant - .cells - .get(sry) - .and_then(|row| row.get(srx)) - .copied() - .unwrap_or(' '); - if ch == '\u{2800}' || ch == ' ' { - continue; - } - let intensity = (0.7 + 0.3 * life_factor).min(1.0); - put_force( - grid, - ins.rect.x + rx, - ins.rect.y + ry, - ch, - (255, 60, 60), - intensity, - ); - } - } -} - -/// The litany — scripture that runs as a single bright row across the very -/// top of the screen, always above everything else. Overwrites whatever zone -/// owned row 0, unbroken by borders. The creed speaks to the whole room. -const LITANY: &str = - " ✦ ONE CURSOR FLOWS AND ALL CELLS AWAKEN ☸ BY GOLDEN ANGLE ALL THINGS ALIGN \ - ✧ PIPES CARRY WHAT CANNOT BE HELD ◉ φ = 1.618 IS THE ARCHITECT \ - ⚘ SIGNAL BECOMES SACRAMENT ✦ AS ABOVE SO BELOW ☸ \ - THE CUBE AT THE EAST KEEPS COUNT ✧ FOLD BY FOLD ◉ \ - HOSE IS HOLY HOSE IS HOLY HOSE IS HOLY ⚘ "; - -fn paint_litany(grid: &mut [Vec], w: i32, cursor: f32) { - let chars: Vec = LITANY.chars().collect(); - let n = chars.len() as i64; - if n == 0 || grid.is_empty() { - return; - } - let scroll = (cursor * 0.35) as i64; - for x in 0..w { - let idx = (scroll + x as i64).rem_euclid(n) as usize; - put_force(grid, x, 0, chars[idx], (255, 50, 50), 1.0); - } -} - -fn paint_pipe( - grid: &mut [Vec], - pipe: &Pipe, - zones: &[Zone], - stream: &[char], - assets: &[ImageAsset], - cursor: f32, -) { - let from_zone = &zones[pipe.from as usize]; - let from_tap = from_zone.tap_offset as i64; - let n = pipe.cells.len(); - let input_end = n / 3; - let op_end = (n * 2) / 3; - let op_syms = transform_symbols(pipe.transform); - let op_len = op_end - input_end; - - // If the source zone is an ImagePanel, the pipe carries its raw luma bytes - // instead of generic stream chars. You literally see the image's pixel - // data flowing across the compression operator. - let image_src: Option<&[u8]> = match from_zone.formation { - Formation::ImagePanel { asset } => assets.get(asset).map(|a| a.luma.as_slice()), - _ => None, - }; - - // Helper: fetch a char at position `p` along the pipe — either from the - // global stream or from the source image's luma buffer encoded as a hex - // digit pair (so the byte-value shape reads as data). - let fetch_char = |p: i64| -> char { - if let Some(bytes) = image_src { - if bytes.is_empty() { - return ' '; - } - let idx = p.rem_euclid(bytes.len() as i64 * 2) as usize; - let byte = bytes[idx / 2]; - let nib = if idx & 1 == 0 { byte >> 4 } else { byte & 0x0f }; - HEX[nib as usize] - } else { - sample(stream, p) - } - }; - - for (i, &(x, y)) in pipe.cells.iter().enumerate() { - let ch = if i < input_end { - fetch_char((cursor as i64) - from_tap - i as i64 * pipe.step) - } else if i < op_end { - let sym_idx = (i - input_end) * op_syms.len() / op_len.max(1); - op_syms[sym_idx.min(2)] - } else { - let delay = (i - input_end) as i64 * pipe.step; - let src_pos = (cursor as i64) - from_tap - delay; - // Apply transform on the char we'd display at input side. - // For image sources, the char is already a hex digit so transforming - // it gives visible XOR/rot/bit-rev/stripe output, reading as - // "encoded image bytes crossing the operator." - let src = fetch_char(src_pos); - apply_transform(src, pipe.transform) - }; - let i_val = if (input_end..op_end).contains(&i) { - 0.92 - } else { - 1.0 - }; - put_force(grid, x, y, ch, (255, 50, 50), i_val); - } -} - -/// Edge-morph pass — after formations paint, cells within 3 of any zone edge -/// have a probability of bleeding in a character + color from a neighbor-owned -/// cell. Creates a soft, shimmering boundary between adjacent zones where -/// the character "languages" morph into each other. Exempts the cube zone. -fn apply_edge_morph(grid: &mut [Vec], scene: &Scene) { - let grid_h = grid.len() as i32; - let grid_w = if grid.is_empty() { - 0 - } else { - grid[0].len() as i32 - }; - - for i in 0..scene.zones.len() { - let z = &scene.zones[i]; - if matches!(z.formation, Formation::RaytraceCube) { - continue; - } - let r = z.base_rect; - let morph_d: i32 = 3; - let t_phase = (scene.cursor * 0.25) as i32; - - for ry in 0..r.h { - for rx in 0..r.w { - let dx = rx.min(r.w - 1 - rx); - let dy = ry.min(r.h - 1 - ry); - let dist = dx.min(dy); - if dist >= morph_d { - continue; - } - - let gx = r.x + rx; - let gy = r.y + ry; - if gx < 0 || gy < 0 || gx >= grid_w || gy >= grid_h { - continue; - } - let (ugx, ugy) = (gx as usize, gy as usize); - if grid[ugy][ugx].owner != i as u16 { - continue; - } - - // Nearness in [0, 1]; squared so effect falls off faster. - let nearness = (morph_d - dist) as f32 / morph_d as f32; - let h_val = ihash(rx, ry, t_phase); - let r_val = (h_val & 0xff) as f32 / 255.0; - let threshold = 0.55 * nearness * nearness; - if r_val >= threshold { - continue; - } - - // Pick a direction outward — one of 4 cardinal dirs weighted - // toward the nearest edge so bleeding mostly comes from the - // neighbor on that side. - let (sdx, sdy): (i32, i32) = if dx < dy { - if rx < r.w / 2 { - (-1, 0) - } else { - (1, 0) - } - } else { - if ry < r.h / 2 { - (0, -1) - } else { - (0, 1) - } - }; - let steps = 1 + ((h_val >> 8) & 0x3) as i32; - let lx = gx + sdx * steps; - let ly = gy + sdy * steps; - if lx < 0 || ly < 0 || lx >= grid_w || ly >= grid_h { - continue; - } - let src = grid[ly as usize][lx as usize]; - // Only morph if the source is owned by a DIFFERENT zone and - // that zone isn't the cube (cube stays crisp). - if src.owner == i as u16 || src.owner == NO_OWNER { - continue; - } - if matches!( - scene.zones[src.owner as usize].formation, - Formation::RaytraceCube - ) { - continue; - } - - let cell = &mut grid[ugy][ugx]; - cell.ch = src.ch; - cell.fg = src.fg; - // Intensity: blend toward source, preserving some of current. - cell.intensity = (cell.intensity * 0.55 + src.intensity * 0.65).min(1.0); - } - } - } -} - -/// Whether a formation is part of the betting UI overlay (paints LAST so it -/// stays on top of the chaos, but uses normal `put` so chaos can still bleed -/// through cells where its intensity beats the UI's). -fn is_ui_formation(f: &Formation) -> bool { - matches!( - f, - Formation::Atm - | Formation::AgentSlot { .. } - | Formation::ChessBoard - | Formation::PayoutButton - | Formation::TerminalInput - ) -} - -fn render(scene: &Scene) -> Vec> { - let mut grid = vec![vec![PxCell::empty(); scene.w as usize]; scene.h as usize]; - let ctx = PaintCtx { - stream: &scene.stream, - cursor: scene.cursor, - zones: &scene.zones, - adjacency: &scene.adjacency, - assets: &scene.assets, - chess_pos: &scene.chess_pos, - balance: scene.balance, - input_buffer: &scene.input_buffer, - }; - // 1) NON-UI formations first — fib zones, image panels, anything that - // forms the chaotic substrate. - for i in 0..scene.zones.len() { - if !is_ui_formation(&scene.zones[i].formation) { - paint_formation(&mut grid, &scene.zones[i], i as u16, &ctx); - } - } - // 2) Edge-morph pass — zone borders bleed their neighbors' chars in. - apply_edge_morph(&mut grid, scene); - // 3) Pipes force-paint on top — the compression machinery between cells. - for p in &scene.pipes { - paint_pipe( - &mut grid, - p, - &scene.zones, - &scene.stream, - &scene.assets, - scene.cursor, - ); - } - // 4) Persistent overlay streamers (orthogonal + crossed diagonals). - for s in &scene.streamers { - paint_streamer( - &mut grid, - s, - scene.w, - scene.h, - &scene.protected_rects, - &scene.stream, - scene.cursor, - ); - } - // 5) Noise projection feeds from edges. - for f in &scene.noise_feeds { - paint_noise_feed(&mut grid, f, &scene.protected_rects, scene.cursor); - } - // 5b) Dither flow worms — block-density trails ambulating through the grids. - for flow in &scene.dither_flows { - paint_flow(&mut grid, flow); - } - // 6) Glitch insertions — random image fragments blasted on top. - for ins in &scene.glitch_inserts { - paint_glitch_insertion(&mut grid, ins, &scene.assets, scene.cursor); - } - // 7) UI zones (chess + ATM + agents + payout + terminal) re-paint LAST - // so the betting interface stays readable, but chaos leaks through - // every cell where the UI's intensity is below the chaos behind it. - for i in 0..scene.zones.len() { - if is_ui_formation(&scene.zones[i].formation) { - paint_formation(&mut grid, &scene.zones[i], i as u16, &ctx); - } - } - // 8) Litany — scripture scrolling across row 0, above everything. - paint_litany(&mut grid, scene.w, scene.cursor); - // 7) Flip: mirror every row horizontally at the very end so the creed - // flips too — the mirror universe has its own scripture. - if scene.flipped { - for row in grid.iter_mut() { - row.reverse(); - } - } - grid -} - -fn dim(c: (u8, u8, u8), i: f32) -> (u8, u8, u8) { - let f = i.clamp(0.0, 1.0); - ( - (c.0 as f32 * f) as u8, - (c.1 as f32 * f) as u8, - (c.2 as f32 * f) as u8, - ) -} - -fn draw(stdout: &mut impl Write, grid: &[Vec]) -> io::Result<()> { - queue!(stdout, cursor::MoveTo(0, 0))?; - let mut last_fg: Option<(u8, u8, u8)> = None; - for (i, row) in grid.iter().enumerate() { - queue!(stdout, cursor::MoveTo(0, i as u16))?; - for cell in row { - let fg = dim(cell.fg, cell.intensity); - if Some(fg) != last_fg { - queue!( - stdout, - SetForegroundColor(Color::Rgb { - r: fg.0, - g: fg.1, - b: fg.2 - }) - )?; - last_fg = Some(fg); - } - queue!(stdout, Print(cell.ch))?; - } - } - queue!(stdout, ResetColor)?; - stdout.flush()?; - Ok(()) -} - -// ───────────────────────── main ───────────────────────── -fn main() -> io::Result<()> { - let mut stdout = io::stdout(); - terminal::enable_raw_mode()?; - execute!( - stdout, - EnterAlternateScreen, - cursor::Hide, - Clear(ClearType::All) - )?; - - let (cols, rows) = terminal::size()?; - let w = (cols as i32).max(60); - let h = ((rows as i32) - 1).max(12); - let mut scene = build_scene(w, h); - - let target = Duration::from_millis(33); - let mut last = Instant::now(); - - let result = (|| -> io::Result<()> { - loop { - if event::poll(Duration::ZERO)? { - if let Event::Key(k) = event::read()? { - match (k.code, k.modifiers) { - // Always-on quit - (KeyCode::Esc, _) => break, - (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => break, - // Toggles moved to Ctrl-modified so plain f/r are typeable. - (KeyCode::Char('f'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.flipped = !scene.flipped - } - (KeyCode::Char('r'), m) if m.contains(KeyModifiers::CONTROL) => { - scene.reversed = !scene.reversed - } - // Backspace edits the live input buffer. - (KeyCode::Backspace, _) => { - scene.input_buffer.pop(); - } - // Enter clears the buffer (treats it as "submit"). - (KeyCode::Enter, _) => { - scene.input_buffer.clear(); - } - // Plain printable chars (no Ctrl) → input buffer. - (KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { - if scene.input_buffer.chars().count() < 30 { - scene.input_buffer.push(c); - } - } - _ => {} - } - } - } - let now = Instant::now(); - let dt = (now - last).as_secs_f32().min(0.1); - last = now; - - tick(&mut scene, dt); - let grid = render(&scene); - draw(&mut stdout, &grid)?; - - let elapsed = last.elapsed(); - if elapsed < target { - std::thread::sleep(target - elapsed); - } - } - Ok(()) - })(); - - execute!(stdout, ResetColor, cursor::Show, LeaveAlternateScreen)?; - terminal::disable_raw_mode()?; - result -} diff --git a/src/animation/differential.rs b/src/animation/differential.rs index b7858b1..24e4d5e 100644 --- a/src/animation/differential.rs +++ b/src/animation/differential.rs @@ -448,7 +448,7 @@ mod tests { let mut renderer = DifferentialRenderer::new(); renderer.last_frame = Some(BrailleGrid::new(10, 10).unwrap()); - let cloned = renderer.clone(); + let cloned = renderer; assert!(cloned.has_previous_frame()); } } diff --git a/src/animation/timing.rs b/src/animation/timing.rs index 7611c46..714047a 100644 --- a/src/animation/timing.rs +++ b/src/animation/timing.rs @@ -418,6 +418,10 @@ impl Default for FrameTimer { } #[cfg(test)] +// Exact float equality is the property under test: these assert exact stored/reset +// values (a 0.5 stop, 0.0 fps after reset, exactly-zero shading for a perpendicular +// light). All are exactly representable, so an epsilon compare would weaken them. +#[allow(clippy::float_cmp)] mod tests { use super::*; diff --git a/src/bin/dotmax_braille.rs b/src/bin/dotmax_braille.rs index 6c6a3fd..bf8c800 100644 --- a/src/bin/dotmax_braille.rs +++ b/src/bin/dotmax_braille.rs @@ -127,7 +127,7 @@ fn main() -> Result<(), Box> { let grid = builder.render()?; let mut unicode = grid.to_unicode_grid(); if invert_output { - for row in unicode.iter_mut() { + for row in &mut unicode { for ch in row.iter_mut() { let bits = (*ch as u32).saturating_sub(0x2800) as u8; *ch = char::from_u32(0x2800 + (!bits as u32)).unwrap_or(*ch); diff --git a/src/chess/board.rs b/src/chess/board.rs index acc2ff4..1651b10 100644 --- a/src/chess/board.rs +++ b/src/chess/board.rs @@ -57,6 +57,12 @@ impl Default for BoardColorScheme { } /// Render a chess position to a BrailleGrid with options. +/// +/// # Errors +/// +/// Returns [`DotmaxError::InvalidDimensions`] if `options.target_width` or +/// `options.target_height` is zero or exceeds the maximum grid size. Dot and color +/// writes are bounds-checked against the grid before being issued. pub fn render_position_with_options( pos: &Chess, options: &RenderOptions, @@ -81,7 +87,7 @@ pub fn render_position_with_options( let square_height_dots = (height_cells * 4) / 8; for rank in Rank::ALL.iter().rev() { - for file in File::ALL.iter() { + for file in &File::ALL { let square = Square::from_coords(*file, *rank); let is_light = (u32::from(*file) + u32::from(*rank)) % 2 != 0; @@ -145,6 +151,11 @@ pub fn render_position_with_options( } /// Render a chess position to a BrailleGrid (legacy simple API). +/// +/// # Errors +/// +/// Propagates errors from [`render_position_with_options`]; the default 32×16 grid +/// is always valid, so this does not fail in practice. pub fn render_position(pos: &Chess) -> Result { render_position_with_options(pos, &RenderOptions::default()) } diff --git a/src/chess/mod.rs b/src/chess/mod.rs index 8c2f126..4fdb556 100644 --- a/src/chess/mod.rs +++ b/src/chess/mod.rs @@ -13,11 +13,21 @@ use shakmaty::{Chess, Position}; /// Render a PGN string to a braille-encoded string. /// /// This is an agnostic output suitable for terminals or web frontends. +/// +/// # Errors +/// +/// Returns [`DotmaxError::TerminalBackend`] if the PGN contains no readable game, +/// or a [`DotmaxError`] from board rendering (invalid grid dimensions). pub fn render_pgn(pgn: &str, move_index: Option) -> Result { render_pgn_with_options(pgn, move_index, &RenderOptions::default()) } /// Render a PGN string to a braille-encoded string with options. +/// +/// # Errors +/// +/// Returns [`DotmaxError::TerminalBackend`] if the PGN contains no readable game, +/// or a [`DotmaxError`] from board rendering (invalid grid dimensions). pub fn render_pgn_with_options( pgn: &str, move_index: Option, diff --git a/src/color/apply.rs b/src/color/apply.rs index af719e9..e7f8703 100644 --- a/src/color/apply.rs +++ b/src/color/apply.rs @@ -233,6 +233,11 @@ fn normalize_intensity(intensity: f32) -> f32 { // ============================================================================ #[cfg(test)] +// Exact float equality is the property under test: normalize_intensity must clamp to +// exactly 0.0/1.0 (including for NaN and the infinities) and be the identity in range. +// Every compared value is exactly representable, so an epsilon comparison would weaken +// these assertions rather than fix them. +#[allow(clippy::float_cmp)] mod tests { use super::*; use crate::color::schemes::{ diff --git a/src/color/convert.rs b/src/color/convert.rs index 8e0b9f4..8e0749f 100644 --- a/src/color/convert.rs +++ b/src/color/convert.rs @@ -974,14 +974,14 @@ mod tests { for code in 0..8 { let escape = ansi16_fg_escape(code); assert!(escape.starts_with("\x1b[3")); - assert!(escape.ends_with("m")); + assert!(escape.ends_with('m')); } // Bright colors (8-15) for code in 8..16 { let escape = ansi16_fg_escape(code); assert!(escape.starts_with("\x1b[9")); - assert!(escape.ends_with("m")); + assert!(escape.ends_with('m')); } } diff --git a/src/color/scheme_builder.rs b/src/color/scheme_builder.rs index 50f1879..0adf7ab 100644 --- a/src/color/scheme_builder.rs +++ b/src/color/scheme_builder.rs @@ -266,6 +266,10 @@ impl ColorSchemeBuilder { } #[cfg(test)] +// Exact float equality is the property under test: these assert exact stored/reset +// values (a 0.5 stop, 0.0 fps after reset, exactly-zero shading for a perpendicular +// light). All are exactly representable, so an epsilon compare would weaken them. +#[allow(clippy::float_cmp)] mod tests { use super::*; @@ -299,7 +303,7 @@ mod tests { let builder = ColorSchemeBuilder::new("clone_test") .add_color(0.0, Color::black()) .add_color(1.0, Color::white()); - let cloned = builder.clone(); + let cloned = builder; assert_eq!(cloned.name, "clone_test"); assert_eq!(cloned.stops.len(), 2); } diff --git a/src/image/color_mode.rs b/src/image/color_mode.rs index af0aeb9..6363817 100644 --- a/src/image/color_mode.rs +++ b/src/image/color_mode.rs @@ -750,6 +750,13 @@ pub fn render_image_with_color( /// Same as [`render_image_with_color`] but accepts per-frame jitter parameters /// for ambient (temporal) dithering. With `JitterParams::NONE` it is identical /// to the deterministic entry point. +/// +/// # Errors +/// +/// Returns [`DotmaxError`] if: +/// - `brightness` or `contrast` is outside `0.0..=2.0`, or `gamma` is outside `0.1..=3.0` +/// - The image is empty (zero width or height) +/// - Grid allocation fails or a color write falls outside the grid #[allow(clippy::too_many_arguments)] pub fn render_image_with_color_jittered( image: &DynamicImage, diff --git a/src/image/mod.rs b/src/image/mod.rs index 9a5c748..c163c62 100644 --- a/src/image/mod.rs +++ b/src/image/mod.rs @@ -166,8 +166,7 @@ fn clock_seed() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now() .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(0xA5A5_5A5A_5A5A_A5A5) + .map_or(0xA5A5_5A5A_5A5A_A5A5, |d| d.as_nanos() as u64) } /// Resize mode configuration for [`ImageRenderer`]. @@ -324,8 +323,8 @@ impl ImageRenderer { /// /// Each `render()` call still advances the frame counter — pin the seed /// when you want a deterministic animation (e.g., generating a fixed set - /// of frames for a preview). Pass `None` (or call [`unpin_ambient_seed`] - /// — see below) to fall back to clock-driven seeding. + /// of frames for a preview). Leave the seed unset (the default) to fall + /// back to clock-driven seeding. #[must_use] pub fn ambient_seed(mut self, seed: u64) -> Self { self.ambient_seed = Some(seed); diff --git a/src/image/temporal.rs b/src/image/temporal.rs index 3b9233a..464625c 100644 --- a/src/image/temporal.rs +++ b/src/image/temporal.rs @@ -557,18 +557,16 @@ impl DotTemporalFilter { pub fn filter(&mut self, dots: &[bool]) -> Vec { let expected_len = self.width * self.height; - // Initialize confidence if needed - if self.confidence.is_none() - || self - .confidence - .as_ref() - .is_some_and(|c| c.len() != expected_len) - { - // Initialize all dots to 0.5 (neutral) - self.confidence = Some(vec![0.5; expected_len]); + // Initialize confidence if absent or sized for a different grid. + // All dots start at 0.5 (neutral). + let conf = self + .confidence + .get_or_insert_with(|| vec![0.5; expected_len]); + if conf.len() != expected_len { + conf.clear(); + conf.resize(expected_len, 0.5); } - let conf = self.confidence.as_mut().unwrap(); let inv_alpha = 1.0 - self.alpha; dots.iter() @@ -669,7 +667,7 @@ impl TemporalCoherence { /// 1. Frame blending (if enabled) /// 2. Hysteresis thresholding (if enabled) /// - /// Note: Dot-level filtering is applied separately via [`process_dots`]. + /// Note: Dot-level filtering is applied separately via [`Self::process_dots`]. /// /// # Arguments /// @@ -722,20 +720,14 @@ impl TemporalCoherence { } // Initialize or resize dot filter - match &mut self.dot_filter { - Some(filter) => { - filter.resize(width, height); - } - None => { - self.dot_filter = Some(DotTemporalFilter::new( - self.config.dot_filter_alpha, - width, - height, - )); - } - } - - self.dot_filter.as_mut().unwrap().filter(dots) + let alpha = self.config.dot_filter_alpha; + let filter = self + .dot_filter + .get_or_insert_with(|| DotTemporalFilter::new(alpha, width, height)); + // No-op when the filter was just created with these dimensions. + filter.resize(width, height); + + filter.filter(dots) } /// Updates the configuration. diff --git a/src/media/apng.rs b/src/media/apng.rs index 0b49f59..5b9c052 100644 --- a/src/media/apng.rs +++ b/src/media/apng.rs @@ -347,9 +347,8 @@ impl ApngPlayer { let previous_canvas = vec![0u8; canvas_size]; // Get terminal size for rendering - let (terminal_width, terminal_height) = crossterm::terminal::size() - .map(|(w, h)| (w as usize, h as usize)) - .unwrap_or((80, 24)); + let (terminal_width, terminal_height) = + crossterm::terminal::size().map_or((80, 24), |(w, h)| (w as usize, h as usize)); // Allocate frame buffer let frame_buffer = vec![0u8; png_reader.output_buffer_size().unwrap_or(canvas_size)]; diff --git a/src/media/gif.rs b/src/media/gif.rs index 67095a6..687f50c 100644 --- a/src/media/gif.rs +++ b/src/media/gif.rs @@ -285,9 +285,8 @@ impl GifPlayer { let previous_canvas = vec![0u8; canvas_size]; // Get terminal size for rendering - let (terminal_width, terminal_height) = crossterm::terminal::size() - .map(|(w, h)| (w as usize, h as usize)) - .unwrap_or((80, 24)); + let (terminal_width, terminal_height) = + crossterm::terminal::size().map_or((80, 24), |(w, h)| (w as usize, h as usize)); Ok(Self { path, diff --git a/src/media/video.rs b/src/media/video.rs index f00d391..ed610c5 100644 --- a/src/media/video.rs +++ b/src/media/video.rs @@ -316,9 +316,8 @@ impl VideoPlayer { let estimated_frame_count = video_duration.map(|d| (d.as_secs_f64() * fps) as usize); // Get terminal size for rendering - let (terminal_width, terminal_height) = crossterm::terminal::size() - .map(|(w, h)| (w as usize, h as usize)) - .unwrap_or((80, 24)); + let (terminal_width, terminal_height) = + crossterm::terminal::size().map_or((80, 24), |(w, h)| (w as usize, h as usize)); // Calculate target pixel dimensions for braille grid // Each braille cell is 2 pixels wide and 4 pixels tall diff --git a/src/media/webcam.rs b/src/media/webcam.rs index a03d083..7a3b590 100644 --- a/src/media/webcam.rs +++ b/src/media/webcam.rs @@ -671,9 +671,8 @@ impl WebcamPlayer { // Calculate optimal capture resolution based on terminal size // Braille cells are 2x4 pixels, so terminal of 200x50 = 400x200 pixels needed // Request slightly higher to allow for aspect ratio adjustment - let (term_width, term_height) = crossterm::terminal::size() - .map(|(w, h)| (w as u32, h as u32)) - .unwrap_or((80, 24)); + let (term_width, term_height) = + crossterm::terminal::size().map_or((80, 24), |(w, h)| (w as u32, h as u32)); // Target pixels needed (with some headroom) let needed_width = term_width * 2; @@ -743,14 +742,11 @@ impl WebcamPlayer { .map_err(|e| map_ffmpeg_error(&device_str, e))?; // Extract input context from the generic context - let input_context = match context { - ffmpeg::format::context::Context::Input(input) => input, - _ => { - return Err(DotmaxError::WebcamError { - device: device_str, - message: "Unexpected output context when opening webcam".to_string(), - }); - } + let ffmpeg::format::context::Context::Input(input_context) = context else { + return Err(DotmaxError::WebcamError { + device: device_str, + message: "Unexpected output context when opening webcam".to_string(), + }); }; // Find video stream @@ -795,9 +791,8 @@ impl WebcamPlayer { }; // Get terminal size - let (terminal_width, terminal_height) = crossterm::terminal::size() - .map(|(w, h)| (w as usize, h as usize)) - .unwrap_or((80, 24)); + let (terminal_width, terminal_height) = + crossterm::terminal::size().map_or((80, 24), |(w, h)| (w as usize, h as usize)); // Calculate target pixel dimensions for braille grid let target_pixel_width = (terminal_width * 2) as u32; @@ -1045,9 +1040,8 @@ impl WebcamPlayer { loop { match self.decoder.receive_frame(&mut self.decoded_frame) { Ok(()) => { - got_frame = true; // Keep draining - we want the LATEST frame - continue; + got_frame = true; } Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::error::EAGAIN => { // No more frames queued in decoder @@ -1094,8 +1088,7 @@ impl WebcamPlayer { return Some(Ok(())); } Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::error::EAGAIN => { - // Need more packets - continue; + // Need more packets - loop around and read another one } Err(e) => { return Some(Err(DotmaxError::WebcamError { diff --git a/src/prelude.rs b/src/prelude.rs index 2dd9639..05383b3 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -230,6 +230,9 @@ mod tests { use super::*; #[test] + // The nested `returns_result` fn exists *because* it always returns `Ok`: it proves the + // re-exported `Result` alias is usable as a return type. Unwrapping it defeats the test. + #[allow(clippy::unnecessary_wraps)] fn test_core_types_accessible() { // Test BrailleGrid let grid = BrailleGrid::new(10, 5).unwrap(); @@ -287,6 +290,9 @@ mod tests { } #[test] + // These bindings are the assertion: they only have to *name* and type-check each re-export. + // Having no runtime effect is the point, so the effect-free-binding lint is expected here. + #[allow(clippy::no_effect_underscore_binding)] fn test_color_types_accessible() { // Test ColorCapability let _cap = ColorCapability::TrueColor; @@ -309,6 +315,8 @@ mod tests { #[test] #[cfg(feature = "image")] + // Effect-free by design: the binding exists only to prove the re-export type-checks. + #[allow(clippy::no_effect_underscore_binding)] fn test_image_types_accessible() { // Test ImageRenderer let _renderer = ImageRenderer::new(); @@ -319,6 +327,8 @@ mod tests { #[test] #[cfg(feature = "image")] + // Effect-free by design: the fn-pointer bindings only assert the re-exported signatures. + #[allow(clippy::no_effect_underscore_binding)] fn test_media_types_accessible() { // Test MediaContent enum variants are accessible // We can't easily construct them without real files, but we can verify @@ -342,6 +352,8 @@ mod tests { #[test] #[cfg(feature = "video")] + // Effect-free by design: the bindings only assert the re-exported types/signatures exist. + #[allow(clippy::no_effect_underscore_binding)] fn test_webcam_types_accessible() { // Test WebcamDevice let device = WebcamDevice::new("/dev/video0", "Test Camera", "Test description"); @@ -368,6 +380,9 @@ mod tests { } #[test] + // `_use_result_alias` must return `Ok` unconditionally: it is a compile-time check that the + // `Result` alias coexists with every other re-export without a naming conflict. + #[allow(clippy::unnecessary_wraps)] fn test_no_naming_conflicts() { // This test verifies that all re-exported items can be used together // without naming conflicts. If this compiles, there are no conflicts. diff --git a/src/progress/easing.rs b/src/progress/easing.rs index d84462c..7b1b52f 100644 --- a/src/progress/easing.rs +++ b/src/progress/easing.rs @@ -328,6 +328,11 @@ pub const ALL_EASINGS: [Easing; 31] = [ ]; #[cfg(test)] +// Exact float equality is the property under test here: the easing curves must +// hit their endpoints exactly (0.0 and 1.0) and lerp must be exact at the +// midpoint. All compared values are exactly representable in f32, so an epsilon +// comparison would weaken the assertions rather than fix them. +#[allow(clippy::float_cmp)] mod tests { use super::*; diff --git a/src/progress/mod.rs b/src/progress/mod.rs index 02686bc..1d25f22 100644 --- a/src/progress/mod.rs +++ b/src/progress/mod.rs @@ -281,9 +281,11 @@ pub mod draw { } /// Draw a single smooth horizontal bar in row `cell_y` filled to `frac` - /// (`0.0..=1.0`) using eighth-width block glyphs — the classic crisp, - /// sub-character-precise progress bar. Mixes full `█` cells with one partial - /// edge glyph for smoothness no braille dot run can match. + /// (`0.0..=1.0`) using eighth-width block glyphs. + /// + /// This is the classic crisp, sub-character-precise progress bar. It mixes + /// full `█` cells with one partial edge glyph for smoothness no braille dot + /// run can match. pub fn hbar(grid: &mut BrailleGrid, cell_y: usize, frac: f32) { let (w, _) = grid.dimensions(); let frac = frac.clamp(0.0, 1.0); diff --git a/src/progress/styles/animals.rs b/src/progress/styles/animals.rs index 0fa957a..327b3e5 100644 --- a/src/progress/styles/animals.rs +++ b/src/progress/styles/animals.rs @@ -301,7 +301,7 @@ impl ProgressStyle for FishSchool { } let front_x = (ctx.eased * w as f32) as usize; // Number of fish scales with bar width. - let n_fish = (w / 8).max(2).min(10); + let n_fish = (w / 8).clamp(2, 10); let amp = (h / 2).saturating_sub(1).max(1) as f32 * 0.8; let mid = h as f32 / 2.0; for i in 0..n_fish { @@ -425,7 +425,7 @@ impl ProgressStyle for RabbitHops { for x in (0..wi).step_by(2) { draw::dot_i(grid, x, ground); } - for k in 0..(wi / 9 + 1) { + for k in 0..=(wi / 9) { let gx = k * 9 + 4; draw::dot_i(grid, gx, ground - 1); draw::dot_i(grid, gx + 1, ground - 2); @@ -773,7 +773,7 @@ impl ProgressStyle for AntMarch { if w == 0 || h == 0 { return Ok(()); } - let n_ants = (w / 7).max(1).min(8); + let n_ants = (w / 7).clamp(1, 8); let base = (h - 1).min(h.saturating_sub(1)); let head_x = (ctx.eased * w as f32) as usize; // Ant spacing: packed in the filled region. @@ -805,7 +805,7 @@ impl ProgressStyle for AntMarch { draw::dot(grid, (ant_x + 2).min(w - 1), base); // Legs (3 pairs): alternate up/down with phase. for leg in 0..3usize { - let leg_y_off = if (leg + leg_up) % 2 == 0 { 0i32 } else { 1i32 }; + let leg_y_off = i32::from((leg + leg_up) % 2 != 0); // Left leg. draw::dot_i( grid, diff --git a/src/progress/styles/architecture.rs b/src/progress/styles/architecture.rs index a5da847..2fd630c 100644 --- a/src/progress/styles/architecture.rs +++ b/src/progress/styles/architecture.rs @@ -196,9 +196,9 @@ impl ProgressStyle for Skyscraper { // Crane: vertical mast above the building top. if floors >= max_floors.saturating_sub(1) || floors > 0 { - let mast_top_y = dh.saturating_sub(floors + 2).max(0) as i32; + let mast_top_y = dh.saturating_sub(floors + 2) as i32; let mast_x = (bld_x1 as i32).min(dw as i32 - 1); - let building_top_y = dh.saturating_sub(floors + 1).max(0) as i32; + let building_top_y = dh.saturating_sub(floors + 1) as i32; // Mast (vertical post). for y in mast_top_y..=building_top_y { draw::dot_i(grid, mast_x, y); @@ -359,7 +359,7 @@ impl ProgressStyle for GothicArch { let base_y = dh.saturating_sub(1) as i32; // How many arches to draw based on width. - let arch_count = ((dw / 10).max(1)).min(5); + let arch_count = (dw / 10).clamp(1, 5); let arch_slot_w = dw / arch_count; for a in 0..arch_count { @@ -1045,7 +1045,7 @@ impl ProgressStyle for Blueprint { if ctx.eased > 0.85 { let dim_p = (ctx.eased - 0.85) / 0.15; // Dimension line along the top. - let y_dim = (dh / 8).saturating_sub(2).max(0); + let y_dim = (dh / 8).saturating_sub(2); let x_end = (dim_p * dw as f32) as usize; draw::hline(grid, 0, x_end.min(dw.saturating_sub(1)), y_dim); // Arrow heads. diff --git a/src/progress/styles/atari.rs b/src/progress/styles/atari.rs index 81a9c98..a81dc99 100644 --- a/src/progress/styles/atari.rs +++ b/src/progress/styles/atari.rs @@ -852,7 +852,7 @@ impl ProgressStyle for LunarLander { let descent_range = surface_y.saturating_sub(6); let lander_y = (ctx.eased * descent_range as f32) as i32; - let bw: i32 = (w as i32 / 8).max(2).min(5); + let bw: i32 = (w as i32 / 8).clamp(2, 5); let bh: i32 = 2.max((h as i32 / 8).min(3)); // Body rectangle. diff --git a/src/progress/styles/biology.rs b/src/progress/styles/biology.rs index 36cc623..776f554 100644 --- a/src/progress/styles/biology.rs +++ b/src/progress/styles/biology.rs @@ -530,7 +530,7 @@ impl ProgressStyle for ProteinFolding { return Ok(()); } - let n_residues = (w / 4).max(4).min(32); + let n_residues = (w / 4).clamp(4, 32); let mid_y = (h / 2) as f32; // Folded target: a compact spiral / blob centered in the bar let blob_r = (w.min(h) as f32 * 0.30).max(2.0); @@ -1008,9 +1008,6 @@ impl ProgressStyle for VirusSpread { return Ok(()); } - // Total tissue cells; infected fraction = eased - let _total = cw * ch; - // Infection spreads in a diagonal wave from top-left // Cell order: sorted by (cx + cy) ascending (Manhattan distance from origin) // We can approximate by iterating and checking if (cx+cy) / (cw+ch-2) <= eased @@ -1072,7 +1069,7 @@ impl ProgressStyle for IonChannels { draw::hline(grid, 0, w.saturating_sub(1), inner_y); // Ion channels: vertical gaps in the membrane with a gate indicator - let n_channels = ((w / 10).max(1)).min(8); + let n_channels = (w / 10).clamp(1, 8); for ci in 0..n_channels { let ch_x = (ci * w / n_channels + w / (n_channels * 2).max(1)).min(w - 1); // Gate open fraction oscillates with time and ci phase diff --git a/src/progress/styles/cars.rs b/src/progress/styles/cars.rs index 88a0b15..b432592 100644 --- a/src/progress/styles/cars.rs +++ b/src/progress/styles/cars.rs @@ -713,17 +713,17 @@ impl ProgressStyle for GearShifter { line(grid, col_xs[0], mid_y, col_xs[col_count - 1], mid_y); // Vertical gate lines from mid_y to each gear slot - for col in 0..col_count { - line(grid, col_xs[col], mid_y, col_xs[col], row_ys[0]); - line(grid, col_xs[col], mid_y, col_xs[col], row_ys[1]); + for &col_x in &col_xs { + line(grid, col_x, mid_y, col_x, row_ys[0]); + line(grid, col_x, mid_y, col_x, row_ys[1]); } // Gate dots at each gear position - for col in 0..col_count { - for row in 0..2usize { - draw::dot_i(grid, col_xs[col] - 1, row_ys[row]); - draw::dot_i(grid, col_xs[col], row_ys[row]); - draw::dot_i(grid, col_xs[col] + 1, row_ys[row]); + for &col_x in &col_xs { + for &row_y in &row_ys { + draw::dot_i(grid, col_x - 1, row_y); + draw::dot_i(grid, col_x, row_y); + draw::dot_i(grid, col_x + 1, row_y); } } diff --git a/src/progress/styles/cellular.rs b/src/progress/styles/cellular.rs index 965f1ae..ea4284e 100644 --- a/src/progress/styles/cellular.rs +++ b/src/progress/styles/cellular.rs @@ -356,9 +356,9 @@ impl ProgressStyle for GameOfLife { // eased controls a left-to-right reveal column. let reveal_x = (ctx.eased * w as f32).round() as usize; - for y in 0..h.min(board.len()) { + for (y, row) in board.iter().enumerate().take(h) { for x in 0..reveal_x.min(w) { - if x < board[y].len() && board[y][x] { + if x < row.len() && row[x] { draw::dot(grid, x, y); } } @@ -390,10 +390,10 @@ impl BriansBrain { fn initial(w: usize, h: usize) -> Vec> { let mut board = vec![vec![0u8; w]; h]; // Scatter some firing seeds using the hash function. - for y in 0..h { - for x in 0..w { + for (y, row) in board.iter_mut().enumerate() { + for (x, cell) in row.iter_mut().enumerate() { let v = hash2(x as u32, y as u32) % 5; - board[y][x] = if v == 0 { 1 } else { 0 }; + *cell = u8::from(v == 0); } } board @@ -424,11 +424,7 @@ impl BriansBrain { } } } - if n == 2 { - 1 - } else { - 0 - } + u8::from(n == 2) } }; } @@ -461,13 +457,11 @@ impl ProgressStyle for BriansBrain { // progress controls a diagonal reveal: cells with (x+y)/max_sum <= eased are shown. let max_sum = (w + h).saturating_sub(2).max(1); - for y in 0..h.min(board.len()) { + for (y, row) in board.iter().enumerate().take(h) { for x in 0..w { let reveal_frac = (x + y) as f32 / max_sum as f32; - if reveal_frac <= ctx.eased { - if x < board[y].len() && board[y][x] == 1 { - draw::dot(grid, x, y); - } + if reveal_frac <= ctx.eased && x < row.len() && row[x] == 1 { + draw::dot(grid, x, y); } } } @@ -643,9 +637,9 @@ impl ProgressStyle for CyclicCA { // Draw cells that are in "high" states (upper half of N_STATES range). let half = Self::N_STATES / 2; - for y in 0..h.min(board.len()) { + for (y, row) in board.iter().enumerate().take(h) { for x in 0..reveal_x.min(w) { - if x < board[y].len() && board[y][x] >= half { + if x < row.len() && row[x] >= half { draw::dot(grid, x, y); } } @@ -711,67 +705,64 @@ impl Wireworld { let total = perimeter.len().max(1); let lit = (fill_frac * total as f32) as usize; - for i in 0..lit.min(total) { - let (x, y) = perimeter[i]; + for &(x, y) in perimeter.iter().take(lit.min(total)) { board[y][x] = Self::CONDUCTOR; } board } - fn inject_electron(board: &mut Vec>, electron_pos: usize) { + fn inject_electron(board: &mut [Vec], electron_pos: usize) { // Walk the perimeter to find the conductor cell at position `electron_pos`. let h = board.len(); let w = if h == 0 { 0 } else { board[0].len() }; let mut idx = 0usize; - 'outer: for pass in 0..2usize { - // Top row. - for x in 0..w { - if board[0][x] == Self::CONDUCTOR { + // Top row. + if let Some(top) = board.first_mut() { + for cell in top { + if *cell == Self::CONDUCTOR { if idx == electron_pos { - board[0][x] = if pass == 0 { Self::HEAD } else { Self::TAIL }; - break 'outer; + *cell = Self::HEAD; + return; } idx += 1; } } - // Right column. - for y in 1..h { - let rx = w.saturating_sub(1); - if board[y][rx] == Self::CONDUCTOR { - if idx == electron_pos { - board[y][rx] = if pass == 0 { Self::HEAD } else { Self::TAIL }; - break 'outer; - } - idx += 1; + } + // Right column. + let rx = w.saturating_sub(1); + for row in board.iter_mut().skip(1) { + if row[rx] == Self::CONDUCTOR { + if idx == electron_pos { + row[rx] = Self::HEAD; + return; } + idx += 1; } - // Bottom row reversed. - if h > 1 { - let by = h - 1; - for x in (0..w.saturating_sub(1)).rev() { - if board[by][x] == Self::CONDUCTOR { - if idx == electron_pos { - board[by][x] = if pass == 0 { Self::HEAD } else { Self::TAIL }; - break 'outer; - } - idx += 1; + } + // Bottom row reversed. + if h > 1 { + let by = h - 1; + for x in (0..w.saturating_sub(1)).rev() { + if board[by][x] == Self::CONDUCTOR { + if idx == electron_pos { + board[by][x] = Self::HEAD; + return; } + idx += 1; } } - // Left column reversed. - if w > 1 && h > 1 { - for y in (1..h.saturating_sub(1)).rev() { - if board[y][0] == Self::CONDUCTOR { - if idx == electron_pos { - board[y][0] = if pass == 0 { Self::HEAD } else { Self::TAIL }; - break 'outer; - } - idx += 1; + } + // Left column reversed. + if w > 1 && h > 1 { + for y in (1..h.saturating_sub(1)).rev() { + if board[y][0] == Self::CONDUCTOR { + if idx == electron_pos { + board[y][0] = Self::HEAD; + return; } + idx += 1; } } - // Only one pass needed — if we're here electron_pos > total conductors. - break; } } @@ -845,10 +836,10 @@ impl ProgressStyle for Wireworld { board = Self::step(&board); } - for y in 0..h.min(board.len()) { + for (y, row) in board.iter().enumerate().take(h) { for x in 0..w { - if x < board[y].len() { - match board[y][x] { + if x < row.len() { + match row[x] { Self::CONDUCTOR => draw::dot(grid, x, y), Self::HEAD => { // Draw HEAD brighter by also dotting adjacent positions. @@ -860,7 +851,7 @@ impl ProgressStyle for Wireworld { draw::dot(grid, x.saturating_sub(1), y); } } - Self::TAIL => {} // tail is invisible (just went dark) + // TAIL is invisible (just went dark), as is EMPTY. _ => {} } } diff --git a/src/progress/styles/chaos.rs b/src/progress/styles/chaos.rs index 6bd1d60..c70b111 100644 --- a/src/progress/styles/chaos.rs +++ b/src/progress/styles/chaos.rs @@ -28,6 +28,8 @@ use std::f32::consts::PI; // Deterministic hash helper for seeding // --------------------------------------------------------------------------- +// Hot path: called per-dot inside every chaos render loop; inlining is deliberate. +#[allow(clippy::inline_always)] #[inline(always)] fn hash(n: u32) -> u32 { let mut x = n.wrapping_mul(2_654_435_761); diff --git a/src/progress/styles/chemistry.rs b/src/progress/styles/chemistry.rs index a2329b5..cb0e53c 100644 --- a/src/progress/styles/chemistry.rs +++ b/src/progress/styles/chemistry.rs @@ -841,12 +841,12 @@ impl ProgressStyle for BoltzmannDistribution { } // Re-check max after shimmer - let max_h = heights.iter().cloned().fold(0.0_f32, f32::max).max(0.001); + let max_h = heights.iter().copied().fold(0.0_f32, f32::max).max(0.001); // Draw each column using vblock glyphs from the bottom up - for col in 0..cw { - let norm = heights[col] / max_h; // [0,1] - // Each column spans ch cells vertically; fill bottom-up + for (col, &height) in heights.iter().enumerate() { + let norm = height / max_h; // [0,1] + // Each column spans ch cells vertically; fill bottom-up let total_eighths = (norm * ch as f32 * 8.0).round() as usize; let full_cells = total_eighths / 8; let partial = total_eighths % 8; @@ -863,7 +863,7 @@ impl ProgressStyle for BoltzmannDistribution { } // Colour: hot bins (near peak) get end-palette colour - let t = heights[col] / max_h; + let t = height / max_h; for cy in 0..ch { draw::tint_row(grid, cy, col, col, ctx.palette.sample(t)); } diff --git a/src/progress/styles/cosmos.rs b/src/progress/styles/cosmos.rs index d9b47d9..4ef0349 100644 --- a/src/progress/styles/cosmos.rs +++ b/src/progress/styles/cosmos.rs @@ -877,11 +877,11 @@ impl ProgressStyle for CosmicWeb { edges.push((i, (i + NUM_NODES as usize / 2) % NUM_NODES as usize)); } // Remove exact duplicates (normalise so a < b). - edges.iter_mut().for_each(|(a, b)| { + for (a, b) in &mut edges { if *a > *b { std::mem::swap(a, b); } - }); + } edges.sort_unstable(); edges.dedup(); diff --git a/src/progress/styles/cultures.rs b/src/progress/styles/cultures.rs index c51113b..c3738b3 100644 --- a/src/progress/styles/cultures.rs +++ b/src/progress/styles/cultures.rs @@ -826,7 +826,7 @@ impl ProgressStyle for PaisleySwirl { } // Number of paisley seeds tiling the bar. - let n_seeds: usize = ((dw / (dh.max(1) * 2)).max(1)).min(12); + let n_seeds: usize = (dw / (dh.max(1) * 2)).clamp(1, 12); let seed_w = dw / n_seeds.max(1); let eased_e = ease(Easing::QuadOut, ctx.eased); @@ -905,7 +905,7 @@ impl ProgressStyle for KenteWeave { } // Strip width in dots (both warp and weft). - let strip = ((dh / 4).max(1)).min(6).min(dw); + let strip = (dh / 4).clamp(1, 6).min(dw); let n_warp = (dw / (strip * 2).max(1)).max(1); let n_weft = (dh / (strip * 2).max(1)).max(1); diff --git a/src/progress/styles/electronics.rs b/src/progress/styles/electronics.rs index fdd4137..869bcaf 100644 --- a/src/progress/styles/electronics.rs +++ b/src/progress/styles/electronics.rs @@ -168,8 +168,8 @@ impl ProgressStyle for Oscilloscope { } // Draw graticule: sparse horizontal and vertical lines. - let h_divs = 4usize.max(1); - let v_divs = 8usize.max(1); + let h_divs = 4usize; + let v_divs = 8usize; for di in 0..=h_divs { let y = di * h / h_divs.max(1); let y = y.min(h - 1); @@ -446,9 +446,9 @@ impl ProgressStyle for SevenSegment { // Determine how many digits we can fit. // Each digit needs ~6 dot-columns wide and full height. - let digit_w = (w / 4).max(3).min(12); + let digit_w = (w / 4).clamp(3, 12); let gap = 2usize; - let n_digits = ((w + gap) / (digit_w + gap)).max(1).min(4); + let n_digits = ((w + gap) / (digit_w + gap)).clamp(1, 4); // Current count: 0..=10^n_digits - 1 let max_val = 10usize.pow(n_digits as u32).saturating_sub(1); @@ -617,7 +617,7 @@ impl ProgressStyle for BinaryBus { if bit { // HIGH: draw a line at top of cell. - draw::hline(grid, x0, x1, y.saturating_sub(1).max(0)); + draw::hline(grid, x0, x1, y.saturating_sub(1)); draw::hline(grid, x0, x1, y); } else { // LOW: draw a line at bottom (just one dot row). @@ -788,9 +788,9 @@ impl ProgressStyle for PwmDuty { let (cw, ch) = grid.dimensions(); for col in 0..cw { let xi_mid = col * 2 + 1; - let phase = (xi_mid + scroll) % (period * 2 / 1).max(2); // dot phase + let phase = (xi_mid + scroll) % (period * 2).max(2); // dot phase let cell_period = period / 2; // cells per period (each cell = 2 dots) - let cell_on = (on_time / 2).max(if on_time > 0 { 1 } else { 0 }); + let cell_on = (on_time / 2).max(usize::from(on_time > 0)); let cell_phase = (col + scroll / 2) % cell_period.max(1); if cell_phase < cell_on && cell_period > 0 { // Use shade to indicate ON time. diff --git a/src/progress/styles/floweroflife.rs b/src/progress/styles/floweroflife.rs index 82bc3f5..13bb4de 100644 --- a/src/progress/styles/floweroflife.rs +++ b/src/progress/styles/floweroflife.rs @@ -23,7 +23,7 @@ fn plot_circle(grid: &mut BrailleGrid, cx: f32, cy: f32, r: f32) { return; } // Circumference ≈ 2πr; sample at ≥2 dots per step so we don't miss any dot. - let steps = ((2.0 * PI * r).ceil() as usize * 2).max(8).min(2048); + let steps = ((2.0 * PI * r).ceil() as usize * 2).clamp(8, 2048); for i in 0..steps { let angle = 2.0 * PI * i as f32 / steps as f32; let px = cx + r * angle.cos(); @@ -36,7 +36,7 @@ fn plot_circle(grid: &mut BrailleGrid, cx: f32, cy: f32, r: f32) { fn plot_line(grid: &mut BrailleGrid, x0: f32, y0: f32, x1: f32, y1: f32) { let dx = x1 - x0; let dy = y1 - y0; - let steps = (dx.abs().max(dy.abs()).ceil() as usize).max(1).min(4096); + let steps = (dx.abs().max(dy.abs()).ceil() as usize).clamp(1, 4096); for i in 0..=steps { let t = i as f32 / steps as f32; let px = x0 + dx * t; @@ -362,7 +362,7 @@ impl ProgressStyle for FruitOfLife { // Phase 1 (eased 0→0.5): reveal circles one by one. // Phase 2 (eased 0.5→1): draw connecting lines between all centres. let circle_frac = (ctx.eased * 2.0).min(1.0); - let line_frac = ((ctx.eased - 0.5) * 2.0).max(0.0).min(1.0); + let line_frac = ((ctx.eased - 0.5) * 2.0).clamp(0.0, 1.0); let reveal_circles = (circle_frac * total as f32).ceil() as usize; for (i, &(px, py)) in centres.iter().enumerate().take(reveal_circles.min(total)) { @@ -568,18 +568,21 @@ impl ProgressStyle for TripodOfLife { // 3 circle centres at 120° intervals. let mut circle_centres = [(0f32, 0f32); 3]; - for i in 0..3 { + for (i, centre) in circle_centres.iter_mut().enumerate() { let a = rot + i as f32 * 2.0 * PI / 3.0; - circle_centres[i] = (cx + r * 0.5 * a.cos(), cy + r * 0.5 * a.sin()); + *centre = (cx + r * 0.5 * a.cos(), cy + r * 0.5 * a.sin()); } // Reveal circles (phase 1: eased 0→0.5), then spoke arms (phase 2: 0.5→1). let circle_frac = (ctx.eased * 2.0).min(1.0); - let arm_frac = ((ctx.eased - 0.5) * 2.0).max(0.0).min(1.0); + let arm_frac = ((ctx.eased - 0.5) * 2.0).clamp(0.0, 1.0); let reveal_circles = (circle_frac * 3.0).ceil() as usize; - for i in 0..reveal_circles.min(3) { - let (px, py) = circle_centres[i]; + for (i, &(px, py)) in circle_centres + .iter() + .enumerate() + .take(reveal_circles.min(3)) + { plot_circle(grid, px, py, r); let color = ctx.palette.sample(i as f32 / 2.0); let (cw, ch) = grid.dimensions(); diff --git a/src/progress/styles/food.rs b/src/progress/styles/food.rs index 96d9a8e..bc01e0e 100644 --- a/src/progress/styles/food.rs +++ b/src/progress/styles/food.rs @@ -249,6 +249,8 @@ impl ProgressStyle for PizzaSlices { } let mut angle = fy.atan2(fx); // Normalise angle into [a0, a1] range. + // atan2 ∈ [-π, π] and a0 < 3π/2, so this runs at most twice. + #[allow(clippy::while_float)] while angle < a0 { angle += 2.0 * PI; } diff --git a/src/progress/styles/fruits.rs b/src/progress/styles/fruits.rs index 4b5fe55..51c2397 100644 --- a/src/progress/styles/fruits.rs +++ b/src/progress/styles/fruits.rs @@ -626,11 +626,7 @@ impl ProgressStyle for PineappleLattice { let ny = (row * cell_y + next_y_off + drift_y) % (h + cell_y); if ny < h { let mid_x = (cx2 + nx) / 2; - let mid_y = if ry < ny { - (ry + ny) / 2 - } else { - (ny + ry) / 2 - }; + let mid_y = (ry + ny) / 2; draw::dot(grid, mid_x.min(w - 1), mid_y.min(h - 1)); } } @@ -913,7 +909,7 @@ impl ProgressStyle for BerryPop { (0.92, 0.5), ]; let n_berries = positions.len(); - let berry_r = (w.min(h * 2) / (n_berries + 2)).max(1).min(3) as i32; + let berry_r = (w.min(h * 2) / (n_berries + 2)).clamp(1, 3) as i32; let visible = (ctx.eased * n_berries as f32).ceil() as usize; // Each berry's progress fraction threshold. for (i, &(xf, yf)) in positions.iter().enumerate() { diff --git a/src/progress/styles/gadgets.rs b/src/progress/styles/gadgets.rs index 56c0242..bf3fc95 100644 --- a/src/progress/styles/gadgets.rs +++ b/src/progress/styles/gadgets.rs @@ -868,7 +868,7 @@ impl ProgressStyle for UsbTransfer { let x0 = x0.min(w.saturating_sub(dev_w + 3)); // Packet shape: small rectangle, 3-dot wide, 2-dot tall. - let pkt_w = (w / 20).max(2).min(4); + let pkt_w = (w / 20).clamp(2, 4); let pkt_h = 2usize.min(h); let py0 = mid.saturating_sub(pkt_h / 2); draw::fill_rect(grid, x0, py0, pkt_w, pkt_h); @@ -946,10 +946,10 @@ impl ProgressStyle for GearTrain { let theta = i as f32 / steps as f32 * 2.0 * PI + angle_offset; // Is this angle at a tooth? let tooth_phase = (theta * n_teeth as f32 / (2.0 * PI)).fract(); - let tooth_bump = if tooth_phase < 0.25 || tooth_phase > 0.75 { - tooth_len - } else { + let tooth_bump = if (0.25..=0.75).contains(&tooth_phase) { 0.0 + } else { + tooth_len }; let r_here = r_f + tooth_bump; let dx = (theta.cos() * r_here) as i32; @@ -977,8 +977,8 @@ impl ProgressStyle for GearTrain { let gear_ratio = big_r as f32 / small_r.max(1) as f32; let small_angle = -ctx.time * omega_big * gear_ratio; - let n_teeth_big = (big_r / 2).max(4).min(16); - let n_teeth_small = (small_r / 2).max(3).min(8); + let n_teeth_big = (big_r / 2).clamp(4, 16); + let n_teeth_small = (small_r / 2).clamp(3, 8); draw_gear(grid, big_cx, big_cy, big_r, n_teeth_big, big_angle); // Only draw small gear if it fits within the grid. @@ -1003,7 +1003,7 @@ impl ProgressStyle for GearTrain { tiny_cx, small_cy, tiny_r, - (tiny_r / 2).max(3).min(6), + (tiny_r / 2).clamp(3, 6), tiny_angle, ); } @@ -1067,11 +1067,10 @@ impl ProgressStyle for EinkRefresh { let linear = cy * cells_w + cx; let h_val = hash(linear as u32 * 13 + 7); let shade_level = match h_val % 5 { - 0 => 1, // ░ - 1 => 2, // ▒ - 2 => 3, // ▓ - 3 | 4 => 4, // █ - _ => 4, + 0 => 1, // ░ + 1 => 2, // ▒ + 2 => 3, // ▓ + _ => 4, // █ }; draw::shade(grid, cx, cy, shade_level); let t = cx as f32 / cells_w.max(1) as f32; diff --git a/src/progress/styles/gameboy.rs b/src/progress/styles/gameboy.rs index a77264a..85cc9fe 100644 --- a/src/progress/styles/gameboy.rs +++ b/src/progress/styles/gameboy.rs @@ -13,6 +13,7 @@ use super::super::draw; use super::super::{BarContext, ProgressStyle}; use crate::{BrailleGrid, DotmaxError}; +use std::cmp::Ordering; use std::f32::consts::PI; /// All styles in the `gameboy` theme. @@ -252,7 +253,7 @@ impl ProgressStyle for TetrisGb { let base_row = ch.saturating_sub(stack_rows); // Tetromino columns vary phase slightly for the ragged skyline. - let col_w = 2usize.max(1); // 2 cells per "block column" + let col_w = 2usize; // 2 cells per "block column" let block_cols = (cw / col_w).max(1); for bc in 0..block_cols { @@ -568,7 +569,7 @@ impl ProgressStyle for Tamagotchi { let pet_y = 1i32; // Walk animation: bobble up/down with time. let walk_frame = ((ctx.time * 4.0) as usize) % 2; - let bob = if walk_frame == 0 { 0i32 } else { 1i32 }; + let bob = i32::from(walk_frame != 0); // Body: oval. let bx = pet_x; @@ -797,16 +798,20 @@ impl ProgressStyle for HeartContainers { for h_idx in 0..max_hearts { let cx_start = h_idx * heart_w; - let heart_shade = if h_idx < full_hearts { - // Fully filled heart. - 4usize - } else if h_idx == full_hearts { - // Partially filled — shade by partial fraction + pulse. - let lvl = (partial_frac * 3.0 + pulse * 0.5) as usize; - (lvl + 1).min(4) - } else { - // Empty heart container. - 2 + let heart_shade = match h_idx.cmp(&full_hearts) { + Ordering::Less => { + // Fully filled heart. + 4usize + } + Ordering::Equal => { + // Partially filled — shade by partial fraction + pulse. + let lvl = (partial_frac * 3.0 + pulse * 0.5) as usize; + (lvl + 1).min(4) + } + Ordering::Greater => { + // Empty heart container. + 2 + } }; // Each heart: top row is 2 bumps (shade 4 at edges, 3 in middle), @@ -816,7 +821,7 @@ impl ProgressStyle for HeartContainers { let bot_row = ch / 2; // Top bumps. - for cy in top_row..top_row + 1 { + for cy in top_row..=top_row { if cy < ch { for off in 0..heart_w { let cx = cx_start + off; @@ -827,7 +832,7 @@ impl ProgressStyle for HeartContainers { } } // Bottom V. - for cy in bot_row..bot_row + 1 { + for cy in bot_row..=bot_row { if cy < ch { // Middle cell shade (V tip). let mid_cx = cx_start + heart_w / 2; @@ -1109,16 +1114,14 @@ impl ProgressStyle for WarioTreasure { ctx.palette.sample(0.9), ); } - if fill_rows > 0 && fill_start <= cy && cy < ch.saturating_sub(1) { - if chest_start < cw { - draw::tint_row( - grid, - cy, - chest_start + 1, - (chest_start + chest_w.saturating_sub(2)).min(cw.saturating_sub(1)), - ctx.palette.sample(ctx.eased), - ); - } + if fill_rows > 0 && fill_start <= cy && cy < ch.saturating_sub(1) && chest_start < cw { + draw::tint_row( + grid, + cy, + chest_start + 1, + (chest_start + chest_w.saturating_sub(2)).min(cw.saturating_sub(1)), + ctx.palette.sample(ctx.eased), + ); } } Ok(()) @@ -1156,7 +1159,7 @@ impl ProgressStyle for LcdPinball { draw::vline(grid, w.saturating_sub(1), 0, h.saturating_sub(1)); // ── Bumpers: shade ovals in a row near the top ──────────────────────── - let bumper_count = (cw / 3).max(1).min(5); + let bumper_count = (cw / 3).clamp(1, 5); let bumper_spacing = cw / (bumper_count + 1); let lit_bumpers = (ctx.eased * bumper_count as f32).round() as usize; diff --git a/src/progress/styles/geometry.rs b/src/progress/styles/geometry.rs index d8c0b23..7d4fbdc 100644 --- a/src/progress/styles/geometry.rs +++ b/src/progress/styles/geometry.rs @@ -377,10 +377,10 @@ impl ProgressStyle for Phyllotaxis { let scale = fit_scale(dw, dh); let golden_angle: f32 = 2.0 * PI * (1.0 - 1.0 / 1.618_033_9); // ≈ 137.508° - let n_max: usize = 400; - let n_plot = (ctx.eased * n_max as f32).round() as usize; + let n_max: f32 = 400.0; + let n_plot = (ctx.eased * n_max).round() as usize; // c chosen so the outermost seed lands near the grid edge. - let c = scale / (n_max as f32).sqrt(); + let c = scale / n_max.sqrt(); let rot = ctx.time * 0.15; for n in 0..n_plot { @@ -563,8 +563,8 @@ impl ProgressStyle for MaurerRose { let k: f32 = 5.0; let d_deg: f32 = 71.0; // step in degrees let d_rad = d_deg * PI / 180.0; - let n_total: usize = 361; // one full revolution in d-degree steps - let n_chords = (ctx.eased * n_total as f32).round() as usize; + let n_total: f32 = 361.0; // one full revolution in d-degree steps + let n_chords = (ctx.eased * n_total).round() as usize; let rot = ctx.time * 0.15; // Compute successive chord endpoints. diff --git a/src/progress/styles/glitch.rs b/src/progress/styles/glitch.rs index da24930..3cf5bf2 100644 --- a/src/progress/styles/glitch.rs +++ b/src/progress/styles/glitch.rs @@ -349,10 +349,8 @@ impl ProgressStyle for Bitcrush { let fc = col as f32; let state = if fc + 1.0 <= lit { 2 // solid - } else if fc < lit { - 1 // popping in } else { - 0 + i32::from(fc < lit) // 1 = popping in, 0 = empty }; if state == 0 { continue; diff --git a/src/progress/styles/goldenratio.rs b/src/progress/styles/goldenratio.rs index 9a76e13..c746ee8 100644 --- a/src/progress/styles/goldenratio.rs +++ b/src/progress/styles/goldenratio.rs @@ -258,15 +258,13 @@ impl ProgressStyle for GoldenSpiral { // Arc start angles — one quarter-circle sweep per square, clockwise. let arc_starts: [f32; 4] = [PI, 3.0 * PI / 2.0, 0.0, PI / 2.0]; - let mut dir_idx = 0usize; // Pivot = arc center in normalised units (starts at origin = grid center). let mut px_n: f32 = 0.0; let mut py_n: f32 = 0.0; let (dcx, dcy) = center(dw, dh); - for i in 0..n_show { - let side = fibs[i]; - let a_start = arc_starts[dir_idx % 4]; + for (i, &side) in fibs.iter().take(n_show).enumerate() { + let a_start = arc_starts[i % 4]; let a_end = a_start - PI / 2.0; // quarter circle, clockwise in screen // Convert normalised pivot to dot-space. @@ -274,7 +272,7 @@ impl ProgressStyle for GoldenSpiral { let arc_cy = dcy + py_n * unit; // norm y positive = down = dot-space y positive // Draw the outline of the square. - let (ddx, ddy) = dirs[dir_idx % 4]; + let (ddx, ddy) = dirs[i % 4]; // The square occupies from (px_n, py_n) extending in the direction // perpendicular to current movement. Just draw the arc; suppress // the square outline for cleaner look (arc alone reads well). @@ -284,7 +282,7 @@ impl ProgressStyle for GoldenSpiral { // For direction 0 (right): square is to the right, corners at // (px_n, py_n), (px_n+s, py_n), (px_n+s, py_n-s), (px_n, py_n-s). // For simplicity, derive from dir and perp. - let perp = match dir_idx % 4 { + let perp = match i % 4 { 0 => (0.0, -1.0), // right-moving: square extends up (norm) 1 => (1.0, 0.0), // down-moving: square extends right 2 => (0.0, 1.0), // left-moving: square extends down @@ -311,7 +309,7 @@ impl ProgressStyle for GoldenSpiral { arc(grid, arc_cx, arc_cy, side * unit, a_start, a_end); // Advance pivot. - let (adx, ady) = match dir_idx % 4 { + let (adx, ady) = match i % 4 { 0 => (side, 0.0), // moved right → pivot goes right by side 1 => (0.0, side), // moved down → pivot goes down by side 2 => (-side, 0.0), // moved left → pivot goes left by side @@ -321,7 +319,6 @@ impl ProgressStyle for GoldenSpiral { // That corner is pivot + current_dir*side (the arc center was at start pivot). px_n += adx; py_n += ady; - dir_idx += 1; } Ok(()) @@ -387,7 +384,7 @@ impl ProgressStyle for GoldenRectangle { drawn += 1; let mut next_rects: Vec<(f32, f32, f32, f32, bool)> = Vec::new(); - let mut current = rects.clone(); + let mut current = rects; for _d in 0..depth.saturating_sub(1) { next_rects.clear(); @@ -438,7 +435,7 @@ impl ProgressStyle for GoldenRectangle { if next_rects.is_empty() { break; } - current = next_rects.clone(); + current.clone_from(&next_rects); } let _ = drawn; Ok(()) @@ -483,12 +480,10 @@ impl ProgressStyle for FibonacciSquares { let mut px_n: f32 = 0.0; let mut py_n: f32 = 0.0; - let mut dir_idx = 0usize; - for i in 0..n_show { - let side = fibs[i]; - let (ddx, ddy) = dirs[dir_idx % 4]; - let (ppx, ppy) = perps[dir_idx % 4]; + for (i, &side) in fibs.iter().take(n_show).enumerate() { + let (ddx, ddy) = dirs[i % 4]; + let (ppx, ppy) = perps[i % 4]; let s = side; // Four corners in norm units. let c0 = (px_n, py_n); @@ -506,7 +501,7 @@ impl ProgressStyle for FibonacciSquares { bresenham(grid, p0x, p0y, p1x, p1y); } // Advance pivot. - let step = match dir_idx % 4 { + let step = match i % 4 { 0 => (side, 0.0), 1 => (0.0, side), 2 => (-side, 0.0), @@ -514,7 +509,6 @@ impl ProgressStyle for FibonacciSquares { }; px_n += step.0; py_n += step.1; - dir_idx += 1; } Ok(()) } @@ -719,17 +713,14 @@ impl ProgressStyle for GoldenGnomon { for _d in 0..depth.saturating_sub(1) { let mut next: Vec<(Pt, Pt, Pt, bool)> = Vec::new(); for &(a, b, c, is_gnomon) in &tris { + // P on AB s.t. AP = 1/PHI * |AB|, for both subdivisions. + let p = lerp_pt(a, b, 1.0 / PHI); if is_gnomon { - // Gnomon subdivision: P on AB s.t. AP = 1/PHI * |AB|. - let p = lerp_pt(a, b, 1.0 / PHI); next.push((p, a, c, true)); // smaller gnomon - next.push((b, p, c, false)); // golden triangle } else { - // Golden-tri: P on AB s.t. AP = 1/PHI * |AB|. - let p = lerp_pt(a, b, 1.0 / PHI); next.push((c, p, a, true)); // gnomon - next.push((b, p, c, false)); // smaller golden-tri } + next.push((b, p, c, false)); // golden triangle } // Draw all triangles at this depth. for &(a, b, c, _) in &next { diff --git a/src/progress/styles/lasers.rs b/src/progress/styles/lasers.rs index a363834..ae1498d 100644 --- a/src/progress/styles/lasers.rs +++ b/src/progress/styles/lasers.rs @@ -135,7 +135,7 @@ impl ProgressStyle for ChargeAndFire { } } else { // Charging core: concentric rings growing outward from core_x. - let rings = ((charge * 6.0) as usize).max(1).min(6); + let rings = ((charge * 6.0) as usize).clamp(1, 6); for r in 0..rings { let radius = r + 1; // Horizontal arms. @@ -287,8 +287,8 @@ impl ProgressStyle for SecurityGrid { let (cells_w, cells_h) = grid.dimensions(); // Grid spacing: number of beams scales with eased. - let h_beams = ((ctx.eased * 5.0 + 1.0) as usize).min(8).max(1); - let v_beams = ((ctx.eased * 3.0 + 1.0) as usize).min(6).max(1); + let h_beams = ((ctx.eased * 5.0 + 1.0) as usize).clamp(1, 8); + let v_beams = ((ctx.eased * 3.0 + 1.0) as usize).clamp(1, 6); // Horizontal beams. for i in 0..h_beams { @@ -389,7 +389,7 @@ impl ProgressStyle for PrismDispersion { ); // Fanned output beams: spread angle increases with eased. - let n_beams = ((ctx.eased * 7.0 + 1.0) as usize).max(1).min(8); + let n_beams = ((ctx.eased * 7.0 + 1.0) as usize).clamp(1, 8); let fan_origin_x = prism_x + tri_h; let fan_origin_y = mid; let spread = (ctx.eased * PI * 0.7).max(0.05); @@ -693,7 +693,7 @@ impl ProgressStyle for FiberPulse { } let (cells_w, cells_h) = grid.dimensions(); - let n_fibers = ((ctx.eased * 6.0 + 1.0) as usize).max(1).min(7); + let n_fibers = ((ctx.eased * 6.0 + 1.0) as usize).clamp(1, 7); let pulse_speed = 0.3 + ctx.eased * 2.5; for f in 0..n_fibers { @@ -730,13 +730,11 @@ impl ProgressStyle for FiberPulse { let py = v_off + fiber_amp * (x_frac * freq * 2.0 * PI + phase_off).sin(); // Intensity falls off from centre of pulse. let dist = (dp as i32 - pulse_half as i32).abs(); + // Core dot. + draw::dot_i(grid, ppx as i32, py as i32); if dist <= 2 { - // Core dot. - draw::dot_i(grid, ppx as i32, py as i32); draw::dot_i(grid, ppx as i32, py as i32 - 1); draw::dot_i(grid, ppx as i32, py as i32 + 1); - } else { - draw::dot_i(grid, ppx as i32, py as i32); } } @@ -863,7 +861,7 @@ impl ProgressStyle for ParticleAccelerator { // Particles: n_particles travel left→right at speed = eased. let speed = 0.4 + ctx.eased * 4.0; - let n_particles = ((ctx.eased * 8.0 + 1.0) as usize).max(1).min(10); + let n_particles = ((ctx.eased * 8.0 + 1.0) as usize).clamp(1, 10); let particle_gap = w / n_particles.max(1); for p in 0..n_particles { @@ -884,7 +882,7 @@ impl ProgressStyle for ParticleAccelerator { for w_step in 1..wake_len { // Probability falls off with distance. if w_step * 3 < wake_len * 2 { - let wx = if pos >= w_step { pos - w_step } else { 0 }; + let wx = pos.saturating_sub(w_step); draw::dot_i(grid, wx as i32, mid as i32); } } @@ -929,7 +927,7 @@ impl ProgressStyle for DiscoFan { let oy = h as i32 - 1; // Number of beams. - let n_beams = ((ctx.eased * 9.0 + 1.0) as usize).max(1).min(10); + let n_beams = ((ctx.eased * 9.0 + 1.0) as usize).clamp(1, 10); // The fan sweeps continuously via time, occupying an arc that grows with eased. let arc = ctx.eased * PI * 0.9 + 0.1; // arc in radians, 0.1..~π·0.9 diff --git a/src/progress/styles/matrix.rs b/src/progress/styles/matrix.rs index 5b48f28..0a6b615 100644 --- a/src/progress/styles/matrix.rs +++ b/src/progress/styles/matrix.rs @@ -284,10 +284,8 @@ impl ProgressStyle for CascadeWipe { 3 } else if depth >= 1.0 { 2 - } else if depth >= 0.0 { - 1 } else { - 0 + usize::from(depth >= 0.0) }; if level > 0 { draw::shade(grid, x, y, level); diff --git a/src/progress/styles/medieval.rs b/src/progress/styles/medieval.rs index ac1bbe2..7410acf 100644 --- a/src/progress/styles/medieval.rs +++ b/src/progress/styles/medieval.rs @@ -213,7 +213,14 @@ impl ProgressStyle for BowDraw { line_dots(grid, nock_x, mid_y, bow_x, stave_bot); // Arrow: shaft from nock leftward to fletching. - if !release { + if release { + // Arrow has flown — draw it travelling rightward off-screen via time. + let flight_x = (bow_x + (ctx.time % 0.8 * w as f32 * 1.5) as i32).min(w as i32 + 4); + draw::dot_i(grid, flight_x, mid_y); + draw::dot_i(grid, flight_x + 1, mid_y); + draw::dot_i(grid, flight_x + 2, mid_y - 1); + draw::dot_i(grid, flight_x + 2, mid_y + 1); + } else { let arrow_len = (w as i32 * 3 / 5).max(2); let arrow_start = nock_x; let arrow_end = (arrow_start - arrow_len).max(bow_x + 2); @@ -232,13 +239,6 @@ impl ProgressStyle for BowDraw { draw::dot_i(grid, arrow_end - 1, mid_y - 1); draw::dot_i(grid, arrow_end - 1, mid_y + 1); } - } else { - // Arrow has flown — draw it travelling rightward off-screen via time. - let flight_x = (bow_x + (ctx.time % 0.8 * w as f32 * 1.5) as i32).min(w as i32 + 4); - draw::dot_i(grid, flight_x, mid_y); - draw::dot_i(grid, flight_x + 1, mid_y); - draw::dot_i(grid, flight_x + 2, mid_y - 1); - draw::dot_i(grid, flight_x + 2, mid_y + 1); } // Tint: warm wood across stave region, highlight at string. @@ -288,12 +288,9 @@ impl ProgressStyle for CastleBuild { let _y1 = h.saturating_sub(course * course_h + 1); // Alternate stone patterns: solid courses and mortar-jointed rows. - if course % 2 == 0 { - // Solid course - draw::fill_rect(grid, 0, y0, w, course_h); - } else { + draw::fill_rect(grid, 0, y0, w, course_h); + if course % 2 != 0 { // Jointed course: solid but with gaps at alternating x positions - draw::fill_rect(grid, 0, y0, w, course_h); // Mortar joints (knock out single dots) — staggered per course let offset = (course / 2) % 2; let joint_spacing = 4usize; @@ -505,10 +502,8 @@ impl ProgressStyle for ShieldCharge { let boss_r = (arm_w / 2).max(1); for dy in -boss_r..=boss_r { for dx in -boss_r..=boss_r { - if dx * dx + dy * dy <= boss_r * boss_r { - if glint { - draw::dot_i(grid, cx + dx, cy + dy); - } + if dx * dx + dy * dy <= boss_r * boss_r && glint { + draw::dot_i(grid, cx + dx, cy + dy); } } } @@ -738,7 +733,7 @@ impl ProgressStyle for TorchFlame { // Handle: bottom third. let handle_h = (h / 3).max(1); let handle_top = (h - handle_h) as i32; - draw::vline(grid, tx as usize, handle_top as usize, (h - 1).max(0)); + draw::vline(grid, tx as usize, handle_top as usize, h - 1); // Torch head: slightly wider. let head_y = handle_top - 2; draw::hline( diff --git a/src/progress/styles/meter.rs b/src/progress/styles/meter.rs index 2e3bdc2..d11a4e5 100644 --- a/src/progress/styles/meter.rs +++ b/src/progress/styles/meter.rs @@ -253,7 +253,7 @@ impl ProgressStyle for RingProgress { // Filled arc — dense dots over eased fraction. let filled_steps = ((r as f32 * 2.0 * PI * ctx.eased.clamp(0.0, 1.0)).round() as usize) - .max(if ctx.eased > 0.0 { 1 } else { 0 }); + .max(usize::from(ctx.eased > 0.0)); for i in 0..=filled_steps { let t = if filled_steps == 0 { 0.0 diff --git a/src/progress/styles/music.rs b/src/progress/styles/music.rs index 964ebc7..84ed664 100644 --- a/src/progress/styles/music.rs +++ b/src/progress/styles/music.rs @@ -888,7 +888,6 @@ impl ProgressStyle for TuningFork { // Vibration amplitude: fades to 0 as eased → 1. let raw_amp = 1.0 - ctx.eased; - let _freq = 440.0_f32; // visually representative; 440 Hz A4 reference pitch let vis_freq = 4.0; // oscillations per second at screen speed let amp = raw_amp * tine_base_sep as f32; diff --git a/src/progress/styles/mythology.rs b/src/progress/styles/mythology.rs index 6b2c8e6..67eb8c6 100644 --- a/src/progress/styles/mythology.rs +++ b/src/progress/styles/mythology.rs @@ -246,7 +246,7 @@ impl ProgressStyle for HydraHeads { let base_y = (h - 1) as i32; // One head per 10%. - let n_heads = ((ctx.eased * 10.0).ceil() as usize).max(1).min(10); + let n_heads = ((ctx.eased * 10.0).ceil() as usize).clamp(1, 10); // Body base: thick horizontal bar at the bottom. let body_w = (w as f32 * 0.4).round() as usize; @@ -344,7 +344,7 @@ impl ProgressStyle for KrakenDepths { // Seafloor. draw::hline(grid, 0, w - 1, floor_y as usize); - let n_tent = ((ctx.eased * 8.0).ceil() as usize).max(1).min(8); + let n_tent = ((ctx.eased * 8.0).ceil() as usize).clamp(1, 8); let tent_spacing = (w / n_tent.max(1)).max(1); for i in 0..n_tent { diff --git a/src/progress/styles/nature.rs b/src/progress/styles/nature.rs index 6159f15..96f53c3 100644 --- a/src/progress/styles/nature.rs +++ b/src/progress/styles/nature.rs @@ -113,7 +113,7 @@ impl ProgressStyle for GrassBlade { return Ok(()); } - let blade_spacing = 3usize.max(1); + let blade_spacing = 3usize; let base_y = h.saturating_sub(1); let max_growth = h.saturating_sub(1); diff --git a/src/progress/styles/nintendo.rs b/src/progress/styles/nintendo.rs index 62d13de..09e1d86 100644 --- a/src/progress/styles/nintendo.rs +++ b/src/progress/styles/nintendo.rs @@ -380,7 +380,7 @@ impl ProgressStyle for TetrisWell { let row_mod = (inner_h - 1 - y) % 4; if row_mod == 3 { // "Mortar" gap — sparse dots. - for x in (1..inner_w + 1).step_by(3) { + for x in (1..=inner_w).step_by(3) { draw::dot(grid, x, y); } } else { @@ -395,7 +395,7 @@ impl ProgressStyle for TetrisWell { if blink && piece_y < inner_h { // Randomise piece shape by time bucket (cycles through shapes). let shape = ((ctx.time * 0.5) as usize) % 5; - let pw = (inner_w.min(8)).max(1); + let pw = inner_w.clamp(1, 8); let px0 = 1 + (inner_w.saturating_sub(pw)) / 2; match shape { 0 => draw::hline(grid, px0, (px0 + 3).min(dw.saturating_sub(1)), piece_y), // I @@ -882,7 +882,7 @@ impl ProgressStyle for DonkeyBarrel { } // ── Girders: horizontal bands ── - let n_girders = ((dh / 3).max(1)).min(4); + let n_girders = (dh / 3).clamp(1, 4); let girder_gap = dh / (n_girders + 1).max(1); for g in 0..n_girders { diff --git a/src/progress/styles/noise.rs b/src/progress/styles/noise.rs index 3d0894c..771074b 100644 --- a/src/progress/styles/noise.rs +++ b/src/progress/styles/noise.rs @@ -820,7 +820,7 @@ impl ProgressStyle for TopoContour { if dw == 0 || dh == 0 { return Ok(()); } - let n_levels = ((ctx.eased * 8.0) as usize).max(1).min(8); + let n_levels = ((ctx.eased * 8.0) as usize).clamp(1, 8); let scale = 3.5 / dw as f32; let sy = 3.0 / dh.max(1) as f32; let t = ctx.time * 0.2; @@ -837,7 +837,7 @@ impl ProgressStyle for TopoContour { // Draw at contour bands: every 1/n_levels interval near an isoline. let band = (n * n_levels as f32).fract(); // A dot is on the isoline if the band value is near 0 or 1. - if band < 0.12 || band > 0.88 { + if !(0.12..=0.88).contains(&band) { draw::dot(grid, dx, dy); } } diff --git a/src/progress/styles/numbertheory.rs b/src/progress/styles/numbertheory.rs index e858eea..3b83534 100644 --- a/src/progress/styles/numbertheory.rs +++ b/src/progress/styles/numbertheory.rs @@ -137,7 +137,7 @@ impl ProgressStyle for Sieve { } // N = number of integers we lay across the width (at least 2). - let n = w.max(2).min(2000); + let n = w.clamp(2, 2000); let revealed = ((ctx.eased * n as f32).round() as usize).min(n); // Build sieve for 1..=n. @@ -166,9 +166,9 @@ impl ProgressStyle for Sieve { }; // Draw: one dot-column per integer. - for k in 1..=revealed { + for (k, &comp) in composite.iter().enumerate().skip(1).take(revealed) { let x = ((k - 1) * w / n).min(w.saturating_sub(1)); - let is_p = !composite[k]; + let is_p = !comp; // Primes: full column; composites: half-height bottom tick. if is_p { draw::vline(grid, x, 0, h.saturating_sub(1)); @@ -214,7 +214,7 @@ impl ProgressStyle for UlamSpiral { let cy = (h / 2) as i32; // Cap N to avoid spiraling off into unreachable cells. - let n = (w * h).min(4000).max(1); + let n = (w * h).clamp(1, 4000); let revealed = ((ctx.eased * n as f32).round() as usize).min(n); // Generate Ulam spiral coords for 1..=revealed. @@ -282,24 +282,23 @@ impl ProgressStyle for PrimeCounting { return Ok(()); } - let n = w.max(2).min(3000); + let n = w.clamp(2, 3000); let revealed_x = ((ctx.eased * n as f32).round() as usize).min(n); // Precompute π(k) for k in 1..=n. let mut pi = 0usize; let mut prime_counts = vec![0usize; n + 1]; - for k in 1..=n { + for (k, count) in prime_counts.iter_mut().enumerate().skip(1) { if is_prime(k as u64) { pi += 1; } - prime_counts[k] = pi; + *count = pi; } let pi_max = prime_counts[n].max(1); // Draw the step curve up to revealed_x. - for k in 1..=revealed_x { + for (k, &count) in prime_counts.iter().enumerate().skip(1).take(revealed_x) { let x = ((k - 1) * w / n).min(w.saturating_sub(1)); - let count = prime_counts[k]; // Map prime count to y (bottom = 0 primes, top = pi_max). let bar_h = (count * h / pi_max).min(h); let y0 = h.saturating_sub(bar_h); @@ -341,7 +340,7 @@ impl ProgressStyle for Collatz { } // Number of seeds = width in dots, max 500. - let n_seeds = w.min(500).max(1); + let n_seeds = w.clamp(1, 500); let revealed = ((ctx.eased * n_seeds as f32).round() as usize).min(n_seeds); // Collatz stopping time for seed k. @@ -428,7 +427,7 @@ impl ProgressStyle for FibonacciSpiral { for (arc_idx, &fib) in fibs.iter().enumerate().take(revealed) { let r = fib as f32; let angle_start = start_angles[arc_idx % 4]; - let steps = ((r * PI / 2.0) as usize).max(4).min(256); + let steps = ((r * PI / 2.0) as usize).clamp(4, 256); for s in 0..=steps { let theta = angle_start + (s as f32 / steps as f32) * (PI / 2.0); @@ -455,12 +454,12 @@ impl ProgressStyle for FibonacciSpiral { // cursor centre — approximate; recompute centre for last arc. let mut cxl = cx; let mut cyl = cy; - for i in 0..last_idx { + for (i, &fib) in fibs.iter().enumerate().take(last_idx) { match i % 4 { - 0 => cyl -= fibs[i] as i32, - 1 => cxl -= fibs[i] as i32, - 2 => cyl += fibs[i] as i32, - _ => cxl += fibs[i] as i32, + 0 => cyl -= fib as i32, + 1 => cxl -= fib as i32, + 2 => cyl += fib as i32, + _ => cxl += fib as i32, } } let px = cxl + (theta.cos() * r) as i32; @@ -518,8 +517,8 @@ impl ProgressStyle for PascalMod { // The Pascal triangle at row r has r+1 non-trivial entries; we // spread them symmetrically across the width. let entries = (r + 1).min(row_len); - for c in 0..entries { - if row[c] % modulus != 0 { + for (c, &val) in row.iter().enumerate().take(entries) { + if val % modulus != 0 { // Map entry position to x. let x = if entries <= 1 { w / 2 @@ -561,7 +560,7 @@ impl ProgressStyle for TotientHistogram { return Ok(()); } - let n = w.max(2).min(2000); + let n = w.clamp(2, 2000); let revealed = ((ctx.eased * n as f32).round() as usize).min(n).max(1); // Compute totient for k in 2..=revealed. @@ -855,7 +854,7 @@ impl ProgressStyle for Recaman { return Ok(()); } - let max_terms = w.min(60).max(2); + let max_terms = w.clamp(2, 60); let revealed = ((ctx.eased * max_terms as f32).round() as usize).clamp(1, max_terms); // Build Recamán sequence. @@ -895,7 +894,7 @@ impl ProgressStyle for Recaman { // Above baseline for backward jumps (a→b where b 0). - let n_bubbles = - ((ctx.eased * 14.0).round() as usize).max(if ctx.progress > 0.0 { 1 } else { 0 }); + let n_bubbles = ((ctx.eased * 14.0).round() as usize).max(usize::from(ctx.progress > 0.0)); for i in 0..n_bubbles { // Each bubble has a fixed column origin spread across the width. @@ -633,7 +632,7 @@ impl ProgressStyle for Seaweed { // Number of fronds filling from left. let filled_w = (ctx.eased * w as f32).round() as usize; - let n_fronds = (filled_w / 2).max(if ctx.progress > 0.0 { 1 } else { 0 }); + let n_fronds = (filled_w / 2).max(usize::from(ctx.progress > 0.0)); for fi in 0..n_fronds { let fx = (fi * filled_w) / n_fronds.max(1); diff --git a/src/progress/styles/penrose.rs b/src/progress/styles/penrose.rs index b9b75d5..398897f 100644 --- a/src/progress/styles/penrose.rs +++ b/src/progress/styles/penrose.rs @@ -19,7 +19,7 @@ use super::super::{BarContext, ProgressStyle}; use crate::{BrailleGrid, Color, DotmaxError}; use std::f32::consts::PI; -const PHI: f32 = 1.6180339887; +const PHI: f32 = 1.618_034; // ──────────────────────────────────────────────────────────────────────────── // Registry @@ -101,6 +101,9 @@ pub fn styles() -> Vec> { // Shared helpers // ──────────────────────────────────────────────────────────────────────────── +/// A triangle tagged with its kind: `(kind, p, q, r)`, vertices in unit space. +type MarkedTri = (bool, [f32; 2], [f32; 2], [f32; 2]); + /// Grid center in dot-space. #[inline] fn center(dw: usize, dh: usize) -> (f32, f32) { @@ -219,7 +222,7 @@ impl ProgressStyle for PenroseP3 { // Each triangle: type=Acute, vertices (p,q,r) in unit space. // Acute triangle: two short sides length 1, long side PHI. // p = center, q & r on the circle at angles (k±36°)*π/180 - let mut tris: Vec<(bool, [f32; 2], [f32; 2], [f32; 2])> = Vec::new(); + let mut tris: Vec = Vec::new(); for k in 0..10usize { let a1 = (k as f32 * 36.0) * PI / 180.0; let a2 = (k as f32 * 36.0 + 36.0) * PI / 180.0; @@ -260,9 +263,7 @@ impl ProgressStyle for PenroseP3 { /// One deflation step for P3 Robinson triangles. /// is_acute=true → "acute" (fat-rhombus) triangle, false → "obtuse" (thin-rhombus). -fn deflate_p3( - tris: Vec<(bool, [f32; 2], [f32; 2], [f32; 2])>, -) -> Vec<(bool, [f32; 2], [f32; 2], [f32; 2])> { +fn deflate_p3(tris: Vec) -> Vec { let mut out = Vec::with_capacity(tris.len() * 2); for (is_acute, p, q, r) in tris { if is_acute { @@ -328,7 +329,7 @@ impl ProgressStyle for PenroseP2 { let reveal_frac = (ctx.eased * 4.0).fract(); // Seed: 5 golden triangles forming a "star" at the origin. - let mut tris: Vec<(bool, [f32; 2], [f32; 2], [f32; 2])> = Vec::new(); + let mut tris: Vec = Vec::new(); for k in 0..5usize { let a_mid = (k as f32 * 72.0 + 90.0) * PI / 180.0; let a_lo = (k as f32 * 72.0 + 90.0 - 36.0) * PI / 180.0; @@ -365,9 +366,7 @@ impl ProgressStyle for PenroseP2 { } } -fn deflate_p2( - tris: Vec<(bool, [f32; 2], [f32; 2], [f32; 2])>, -) -> Vec<(bool, [f32; 2], [f32; 2], [f32; 2])> { +fn deflate_p2(tris: Vec) -> Vec { let mut out = Vec::with_capacity(tris.len() * 2); for (is_gt, p, q, r) in tris { if is_gt { @@ -424,7 +423,7 @@ impl ProgressStyle for SunPattern { // right_wing = (cos(a-36°), sin(a-36°)) * (1/PHI) // Number of concentric "rings" to draw (1-3 based on eased). - let rings = ((ctx.eased * 3.0) as usize).max(1).min(3); + let rings = ((ctx.eased * 3.0) as usize).clamp(1, 3); for ring in 0..rings { let ring_scale = scale / (1.0 + ring as f32 * 0.6); @@ -586,7 +585,7 @@ impl ProgressStyle for AmmannBars { let step = if use_long { l_step } else { s_step }; // Update Fibonacci-like counter (Beatty sequence approximation). let old_a = fib_a; - fib_a = fib_a + fib_b; + fib_a += fib_b; fib_b = old_a; let fib_a_c = fib_a; let fib_b_c = fib_b; @@ -659,13 +658,12 @@ impl ProgressStyle for DeBruijnPentagrid { let gammas: [f32; 5] = [0.1, 0.2, -0.15, 0.05, -0.08]; // irrational offsets let n_families = (ctx.eased * 5.0).ceil() as usize; - for fam in 0..n_families.min(5) { + for (fam, &gamma) in gammas.iter().enumerate().take(n_families.min(5)) { let angle = fam as f32 * 72.0 * PI / 180.0 + rot; let perp_x = angle.cos(); let perp_y = -angle.sin(); let line_x = -angle.sin(); let line_y = -angle.cos(); - let gamma = gammas[fam]; // Draw ~9 parallel lines (4 on each side of center). let lines = 9i32; diff --git a/src/progress/styles/perspective.rs b/src/progress/styles/perspective.rs index 8b216b1..474ad02 100644 --- a/src/progress/styles/perspective.rs +++ b/src/progress/styles/perspective.rs @@ -1016,7 +1016,7 @@ impl ProgressStyle for ParallaxLayers { if dot_y < dh { draw::dot(grid, px, dot_y); } - if y >= py + 1 { + if y > py { draw::dot(grid, px, y - py - 1); } } diff --git a/src/progress/styles/physics.rs b/src/progress/styles/physics.rs index 17db376..2da0814 100644 --- a/src/progress/styles/physics.rs +++ b/src/progress/styles/physics.rs @@ -551,7 +551,6 @@ impl ProgressStyle for TerminalVelocity { return Ok(()); } let wf = w as f32; - let _hf = h as f32; // Physics: τ = 1 (normalised), v_t = 1 let tau = 1.0_f32; @@ -1120,7 +1119,7 @@ impl ProgressStyle for Doppler { } // Draw circle outline (dot approximation) - let circ_steps = ((radius * PI * 2.0) as usize + 8).max(8).min(128); + let circ_steps = ((radius * PI * 2.0) as usize + 8).clamp(8, 128); let mut prev_c: Option<(i32, i32)> = None; for s in 0..=circ_steps { let angle = s as f32 / circ_steps as f32 * 2.0 * PI; diff --git a/src/progress/styles/plants.rs b/src/progress/styles/plants.rs index 327eee1..320994d 100644 --- a/src/progress/styles/plants.rs +++ b/src/progress/styles/plants.rs @@ -406,7 +406,7 @@ impl ProgressStyle for IvyTrellis { } // Draw trellis: vertical posts every 8 dots, two horizontal rails. - let post_spacing = 8usize.max(1); + let post_spacing = 8usize; let rail1 = h / 4; let rail2 = 3 * h / 4; // Top rail. @@ -446,7 +446,7 @@ impl ProgressStyle for IvyTrellis { } // Tendrils: small clockwise spirals hanging off the bottom vine. - let tendril_spacing = 12usize.max(1); + let tendril_spacing = 12usize; let tendril_count = vine_end_dot / tendril_spacing; for t_idx in 0..tendril_count { let tx = (t_idx * tendril_spacing + tendril_spacing / 2).min(w.saturating_sub(1)); diff --git a/src/progress/styles/platonic.rs b/src/progress/styles/platonic.rs index ad1ea29..a225cf3 100644 --- a/src/progress/styles/platonic.rs +++ b/src/progress/styles/platonic.rs @@ -31,20 +31,32 @@ use crate::{BrailleGrid, Color, DotmaxError}; // ── Shared 3-D helpers ──────────────────────────────────────────────────────── -/// Rotate `(x, y, z)` about X by `ax` then Y by `ay` (extrinsic Euler XY), -/// then orthographically project onto the dot lattice centred at `(cx, cy)`. +/// Euler angles `(ax, ay)`, dot-space centre `(cx, cy)` and uniform `scale` in +/// dots-per-unit — the camera every projected vertex is pushed through. +#[derive(Clone, Copy)] +struct View { + ax: f32, + ay: f32, + cx: i32, + cy: i32, + scale: f32, +} + +/// Rotate `(x, y, z)` about X by `view.ax` then Y by `view.ay` (extrinsic Euler +/// XY), then orthographically project onto the dot lattice centred at +/// `(view.cx, view.cy)`. /// /// Returns `(screen_x, screen_y)` as `i32` for `draw::dot_i`. #[inline] -fn project(x: f32, y: f32, z: f32, ax: f32, ay: f32, cx: i32, cy: i32, scale: f32) -> (i32, i32) { - let (sax, cax) = ax.sin_cos(); +fn project(x: f32, y: f32, z: f32, view: &View) -> (i32, i32) { + let (sax, cax) = view.ax.sin_cos(); let y1 = y * cax - z * sax; let z1 = y * sax + z * cax; - let (say, cay) = ay.sin_cos(); + let (say, cay) = view.ay.sin_cos(); let x2 = x * cay + z1 * say; let y2 = y1; - let sx = cx + (x2 * scale).round() as i32; - let sy = cy - (y2 * scale).round() as i32; + let sx = view.cx + (x2 * view.scale).round() as i32; + let sy = view.cy - (y2 * view.scale).round() as i32; (sx, sy) } @@ -90,19 +102,12 @@ fn grid_centre_scale(grid: &BrailleGrid, shrink: f32) -> (i32, i32, f32) { (cx, cy, scale) } -/// Project a slice of 3-D vertices with given rotation angles, returning -/// screen-space `(i32, i32)` for each. -fn project_verts( - verts: &[[f32; 3]], - ax: f32, - ay: f32, - cx: i32, - cy: i32, - scale: f32, -) -> Vec<(i32, i32)> { +/// Project a slice of 3-D vertices through `view`, returning screen-space +/// `(i32, i32)` for each. +fn project_verts(verts: &[[f32; 3]], view: &View) -> Vec<(i32, i32)> { verts .iter() - .map(|&[x, y, z]| project(x, y, z, ax, ay, cx, cy, scale)) + .map(|&[x, y, z]| project(x, y, z, view)) .collect() } @@ -150,7 +155,14 @@ impl ProgressStyle for Tetrahedron { let (cx, cy, scale) = grid_centre_scale(grid, 1.0); let ax = ctx.time * 0.41; let ay = ctx.time * 0.63; - let pts = project_verts(&TETRA_VERTS, ax, ay, cx, cy, scale); + let view = View { + ax, + ay, + cx, + cy, + scale, + }; + let pts = project_verts(&TETRA_VERTS, &view); let n_show = (ctx.eased * TETRA_EDGES.len() as f32).ceil() as usize; draw_edges_partial(grid, &pts, &TETRA_EDGES, n_show); Ok(()) @@ -207,7 +219,14 @@ impl ProgressStyle for Cube { let (cx, cy, scale) = grid_centre_scale(grid, 1.0); let ax = ctx.time * 0.37; let ay = ctx.time * 0.51; - let pts = project_verts(&CUBE_VERTS, ax, ay, cx, cy, scale); + let view = View { + ax, + ay, + cx, + cy, + scale, + }; + let pts = project_verts(&CUBE_VERTS, &view); let n_show = (ctx.eased * CUBE_EDGES.len() as f32).ceil() as usize; draw_edges_partial(grid, &pts, &CUBE_EDGES, n_show); Ok(()) @@ -256,7 +275,14 @@ impl ProgressStyle for Octahedron { let (cx, cy, scale) = grid_centre_scale(grid, 1.0); let ax = ctx.time * 0.44; let ay = ctx.time * 0.59; - let pts = project_verts(&OCTA_VERTS, ax, ay, cx, cy, scale); + let view = View { + ax, + ay, + cx, + cy, + scale, + }; + let pts = project_verts(&OCTA_VERTS, &view); let n_show = (ctx.eased * OCTA_EDGES.len() as f32).ceil() as usize; draw_edges_partial(grid, &pts, &OCTA_EDGES, n_show); Ok(()) @@ -361,7 +387,14 @@ impl ProgressStyle for Dodecahedron { let (cx, cy, scale) = grid_centre_scale(grid, DODECA_SCALE); let ax = ctx.time * 0.29; let ay = ctx.time * 0.47; - let pts = project_verts(&DODECA_VERTS, ax, ay, cx, cy, scale); + let view = View { + ax, + ay, + cx, + cy, + scale, + }; + let pts = project_verts(&DODECA_VERTS, &view); let n_show = (ctx.eased * DODECA_EDGES.len() as f32).ceil() as usize; draw_edges_partial(grid, &pts, &DODECA_EDGES, n_show); Ok(()) @@ -374,7 +407,7 @@ impl ProgressStyle for Dodecahedron { // ───────────────────────────────────────────────────────────────────────────── /// Circumradius of icosahedron with these coords = √(1+φ²) ≈ 1.902. -const ICOSA_SCALE: f32 = 1.0 / 1.902_113_0; +const ICOSA_SCALE: f32 = 1.0 / 1.902_113; const ICOSA_VERTS: [[f32; 3]; 12] = [ // (0, ±1, ±φ). @@ -448,7 +481,14 @@ impl ProgressStyle for Icosahedron { let (cx, cy, scale) = grid_centre_scale(grid, ICOSA_SCALE); let ax = ctx.time * 0.33; let ay = ctx.time * 0.54; - let pts = project_verts(&ICOSA_VERTS, ax, ay, cx, cy, scale); + let view = View { + ax, + ay, + cx, + cy, + scale, + }; + let pts = project_verts(&ICOSA_VERTS, &view); let n_show = (ctx.eased * ICOSA_EDGES.len() as f32).ceil() as usize; draw_edges_partial(grid, &pts, &ICOSA_EDGES, n_show); Ok(()) @@ -462,7 +502,6 @@ impl ProgressStyle for Icosahedron { /// The merkaba has two interlocked tetrahedra. One uses the canonical upward /// tetrahedron; the other is its inversion (downward — dual). They counter-rotate /// with time so the Merkaba field animates distinctly even without edge-reveal. - struct Merkaba; impl ProgressStyle for Merkaba { fn name(&self) -> &str { @@ -485,10 +524,24 @@ impl ProgressStyle for Merkaba { let ay_dn = -ctx.time * 0.57; // Upward tetrahedron (same as TETRA_VERTS). - let pts_up = project_verts(&TETRA_VERTS, ax_up, ay_up, cx, cy, scale); + let view_up = View { + ax: ax_up, + ay: ay_up, + cx, + cy, + scale, + }; + let pts_up = project_verts(&TETRA_VERTS, &view_up); // Downward tetrahedron (invert y). + let view_dn = View { + ax: ax_dn, + ay: ay_dn, + cx, + cy, + scale, + }; let tetra_down: [[f32; 3]; 4] = TETRA_VERTS.map(|[x, y, z]| [x, -y, z]); - let pts_dn = project_verts(&tetra_down, ax_dn, ay_dn, cx, cy, scale); + let pts_dn = project_verts(&tetra_down, &view_dn); // Reveal first tetrahedron on eased 0→0.5, second on 0.5→1. let total_edges = TETRA_EDGES.len() * 2; @@ -525,16 +578,27 @@ impl ProgressStyle for StarOctangulum { // Both tetrahedra share the same rotation (co-rotating, not counter-rotating). let ax = ctx.time * 0.35; let ay = ctx.time * 0.52; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; // Upward tet — scaled to circumradius 1. - let pts_up = project_verts(&TETRA_VERTS, ax, ay, cx, cy, scale); + let pts_up = project_verts(&TETRA_VERTS, &view); // Downward tet — invert all three axes for the dual orientation. let tetra_dual: [[f32; 3]; 4] = TETRA_VERTS.map(|[x, y, z]| [-x, -y, -z]); - let pts_dn = project_verts(&tetra_dual, ax, ay, cx, cy, scale); + let pts_dn = project_verts(&tetra_dual, &view); // Also draw the inner octahedron formed by the intersection. // Octahedron vertices are midpoints of the stella's edges (unit sphere). - let pts_oct = project_verts(&OCTA_VERTS, ax, ay, cx, cy, scale * 0.577_350_3); + let view_oct = View { + scale: scale * 0.577_350_3, + ..view + }; + let pts_oct = project_verts(&OCTA_VERTS, &view_oct); let total = TETRA_EDGES.len() * 2 + OCTA_EDGES.len(); let n_show = (ctx.eased * total as f32).ceil() as usize; @@ -575,7 +639,7 @@ const CUBOCTA_VERTS: [[f32; 3]; 12] = [ ]; /// Cuboctahedron circumradius = √2, so shrink. -const CUBOCTA_SCALE: f32 = 1.0 / 1.414_213_6; +const CUBOCTA_SCALE: f32 = 1.0 / std::f32::consts::SQRT_2; /// 24 edges of the cuboctahedron. Each vertex has degree 4. /// Two vertices are adjacent iff their distance = √2 (= edge length here). @@ -626,7 +690,14 @@ impl ProgressStyle for Cuboctahedron { let (cx, cy, scale) = grid_centre_scale(grid, CUBOCTA_SCALE); let ax = ctx.time * 0.31; let ay = ctx.time * 0.49; - let pts = project_verts(&CUBOCTA_VERTS, ax, ay, cx, cy, scale); + let view = View { + ax, + ay, + cx, + cy, + scale, + }; + let pts = project_verts(&CUBOCTA_VERTS, &view); let n_show = (ctx.eased * CUBOCTA_EDGES.len() as f32).ceil() as usize; draw_edges_partial(grid, &pts, &CUBOCTA_EDGES, n_show); Ok(()) @@ -656,14 +727,28 @@ impl ProgressStyle for NestedSolids { // Outer octahedron — slower rotation, revealed in first half of eased. let ax_oct = ctx.time * 0.27; let ay_oct = ctx.time * 0.41; - let pts_oct = project_verts(&OCTA_VERTS, ax_oct, ay_oct, cx, cy, scale); + let view_oct = View { + ax: ax_oct, + ay: ay_oct, + cx, + cy, + scale, + }; + let pts_oct = project_verts(&OCTA_VERTS, &view_oct); // Inner cube — faster rotation, revealed in second half, scaled to inradius. // Inradius of octahedron = 1/√3 ≈ 0.577, so cube circumradius ≈ 0.577. let cube_inner_scale = scale * 0.577_350_3; // fit cube inside octahedron let ax_cube = ctx.time * 0.55; let ay_cube = ctx.time * 0.71; - let pts_cube = project_verts(&CUBE_VERTS, ax_cube, ay_cube, cx, cy, cube_inner_scale); + let view_cube = View { + ax: ax_cube, + ay: ay_cube, + cx, + cy, + scale: cube_inner_scale, + }; + let pts_cube = project_verts(&CUBE_VERTS, &view_cube); let n_show = (ctx.eased * (OCTA_EDGES.len() + CUBE_EDGES.len()) as f32).ceil() as usize; let n_oct = n_show.min(OCTA_EDGES.len()); @@ -701,13 +786,24 @@ impl ProgressStyle for StellatedDodecahedron { let (cx, cy, scale) = grid_centre_scale(grid, DODECA_SCALE * 0.72); let ax = ctx.time * 0.25; let ay = ctx.time * 0.43; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; // Draw the dodecahedron base skeleton first. - let pts_dodeca = project_verts(&DODECA_VERTS, ax, ay, cx, cy, scale); + let pts_dodeca = project_verts(&DODECA_VERTS, &view); // The 12 spike tips — icosahedron vertices scaled outward by φ. let spike_scale = scale * PHI * ICOSA_SCALE; - let pts_spikes = project_verts(&ICOSA_VERTS, ax, ay, cx, cy, spike_scale); + let view_spikes = View { + scale: spike_scale, + ..view + }; + let pts_spikes = project_verts(&ICOSA_VERTS, &view_spikes); // Each spike tip connects to the 5 nearest dodecahedron vertices. // For simplicity: each icosahedron vertex (face centre) connects to the @@ -891,6 +987,14 @@ impl ProgressStyle for UnfoldingNet { // Cube spans ±0.5. Map net scale to roughly 0.4 → 1.0 of display scale. let net_s = scale / 3.0; let cube_s = scale; + // Scale: lerp from net_s to cube_s. + let view = View { + ax, + ay, + cx, + cy, + scale: net_s + (cube_s - net_s) * t, + }; for fi in 0..6usize { // Interpolate each corner between net and cube. @@ -901,9 +1005,7 @@ impl ProgressStyle for UnfoldingNet { let x = nx + (cx2 - nx) * t; let y = ny + (cy2 - ny) * t; let z = nz + (cz - nz) * t; - // Scale: lerp from net_s to cube_s. - let s = net_s + (cube_s - net_s) * t; - project(x, y, z, ax, ay, cx, cy, s) + project(x, y, z, &view) }) .collect(); diff --git a/src/progress/styles/quantum.rs b/src/progress/styles/quantum.rs index e8e1206..5aa268d 100644 --- a/src/progress/styles/quantum.rs +++ b/src/progress/styles/quantum.rs @@ -762,7 +762,7 @@ impl ProgressStyle for EnergyLevels { let dhf = dh as f32; // Number of levels — scale with height - let n_levels = ((dh / 3).max(2)).min(8) as usize; + let n_levels = (dh / 3).clamp(2, 8) as usize; // Each level holds 2 electrons (spin up/down) let n_electrons_max = n_levels * 2; let n_filled = (ctx.eased * n_electrons_max as f32).round() as usize; @@ -938,7 +938,7 @@ impl ProgressStyle for QuantumWalk { let mut densities = vec![0.0_f32; cw]; let mut max_d = 0.0_f32; - for cx in 0..cw { + for (cx, density) in densities.iter_mut().enumerate() { // Map column to position x ∈ [-nf, nf] let x = (cx as f32 / cwf - 0.5) * 2.0 * nf; // QW envelope: arcsine law ≈ (n² - x²)^(-1/2), zero outside @@ -947,15 +947,15 @@ impl ProgressStyle for QuantumWalk { // Animated interference ripple inside the distribution let ripple = 1.0 + 0.25 * (PI * x / sigma.max(0.1) + drift_phase).cos(); let d = envelope * ripple.max(0.0); - densities[cx] = d; + *density = d; if d > max_d { max_d = d; } } let max_d = max_d.max(1e-9); - for cx in 0..cw { - let norm = (densities[cx] / max_d).clamp(0.0, 1.0); + for (cx, &density) in densities.iter().enumerate() { + let norm = (density / max_d).clamp(0.0, 1.0); // vblock per cell row for cy in 0..ch { let row_thresh = 1.0 - cy as f32 / chf; diff --git a/src/progress/styles/spinner.rs b/src/progress/styles/spinner.rs index ec751f5..22a89ad 100644 --- a/src/progress/styles/spinner.rs +++ b/src/progress/styles/spinner.rs @@ -334,7 +334,7 @@ impl ProgressStyle for Bounce { if i >= 8 && i % 2 == 1 { continue; } - let half: i32 = if i < 3 { 1 } else { 0 }; + let half: i32 = i32::from(i < 3); for dy in -half..=half { draw::dot_i(grid, tx, cy + dy); } diff --git a/src/progress/styles/sports.rs b/src/progress/styles/sports.rs index 03f5fa0..64b1f9c 100644 --- a/src/progress/styles/sports.rs +++ b/src/progress/styles/sports.rs @@ -447,7 +447,7 @@ impl ProgressStyle for SwimmingLaps { } else { swim_x as i32 + 2 }; - let kick_off = (stroke * -1.0).round() as i32; + let kick_off = (-stroke).round() as i32; draw::dot_i(grid, kick_x, sy as i32 + kick_off); draw::dot_i(grid, kick_x, sy as i32 + kick_off + 1); @@ -499,7 +499,7 @@ impl ProgressStyle for Archery { // Bullseye target: concentric rings on the right. let target_cx = w.saturating_sub(3) as i32; let target_cy = mid as i32; - let max_r = ((h / 2).min(3)).max(1) as i32; + let max_r = (h / 2).clamp(1, 3) as i32; for r in (1..=max_r).rev() { // Approximate circle with 8 cardinal dots. for &(dx, dy) in &[ @@ -691,7 +691,7 @@ impl ProgressStyle for Darts { // Dartboard: concentric dot-rings on the right, centered. let board_cx = w.saturating_sub(2) as i32; let board_cy = mid as i32; - let n_rings = ((h / 2).min(4)).max(1); + let n_rings = (h / 2).clamp(1, 4); for r in 1..=n_rings { let radius = r as i32; // Draw partial ellipse (wider than tall) using parametric dots. @@ -1084,7 +1084,7 @@ impl ProgressStyle for CyclingPeloton { let lead_x = lead_x.min(w.saturating_sub(1)); // Number of riders: scales with bar width, minimum 2. - let n_riders = (w / 10).max(2).min(8); + let n_riders = (w / 10).clamp(2, 8); // Pack depth behind leader. let pack_depth = (n_riders as f32 * 5.0) as usize; diff --git a/src/progress/styles/tech.rs b/src/progress/styles/tech.rs index cedc27b..53fa8fe 100644 --- a/src/progress/styles/tech.rs +++ b/src/progress/styles/tech.rs @@ -8,6 +8,7 @@ use super::super::draw; use super::super::{BarContext, ProgressStyle}; use crate::{BrailleGrid, DotmaxError}; +use std::cmp::Ordering; use std::f32::consts::PI; // ─── deterministic hash (no external crates) ──────────────────────────────── @@ -362,18 +363,22 @@ impl ProgressStyle for HexFill { let x0 = i * cell_w; // Gap of 1 dot between cells. let bw = cell_w.saturating_sub(1).max(1); - if i < lit { - // Fully lit cell. - draw::fill_rect(grid, x0, 0, bw, h); - } else if i == lit { - // Partially lit cell — animates in with a sine flicker. - let flicker = ((ctx.time * 8.0).sin() * 0.5 + 0.5).clamp(0.0, 1.0); - let bh = (flicker * h as f32) as usize; - let y0 = h.saturating_sub(bh); - draw::fill_rect(grid, x0, y0, bw, bh); - } else { - // Unlit: just outline. - draw::rect_outline(grid, x0, 0, bw.max(2), h.max(2)); + match i.cmp(&lit) { + Ordering::Less => { + // Fully lit cell. + draw::fill_rect(grid, x0, 0, bw, h); + } + Ordering::Equal => { + // Partially lit cell — animates in with a sine flicker. + let flicker = ((ctx.time * 8.0).sin() * 0.5 + 0.5).clamp(0.0, 1.0); + let bh = (flicker * h as f32) as usize; + let y0 = h.saturating_sub(bh); + draw::fill_rect(grid, x0, y0, bw, bh); + } + Ordering::Greater => { + // Unlit: just outline. + draw::rect_outline(grid, x0, 0, bw.max(2), h.max(2)); + } } // Tint lit cells. diff --git a/src/progress/styles/topology.rs b/src/progress/styles/topology.rs index 7b53c1d..8a66061 100644 --- a/src/progress/styles/topology.rs +++ b/src/progress/styles/topology.rs @@ -33,44 +33,45 @@ use std::f32::consts::PI; // ── Shared 3-D projection helper ───────────────────────────────────────────── -/// Rotate `(x, y, z)` about the X-axis by `ax` radians then the Y-axis by -/// `ay` radians (standard Euler XY extrinsic), then orthographically project -/// the result onto the dot lattice centred at `(cx, cy)` with uniform `scale` -/// dots-per-unit. +/// Euler angles `(ax, ay)`, dot-space centre `(cx, cy)` and uniform `scale` in +/// dots-per-unit — the camera every projected point is pushed through. +#[derive(Clone, Copy)] +struct View { + ax: f32, + ay: f32, + cx: i32, + cy: i32, + scale: f32, +} + +/// Rotate `(x, y, z)` about the X-axis by `view.ax` radians then the Y-axis by +/// `view.ay` radians (standard Euler XY extrinsic), then orthographically project +/// the result onto the dot lattice centred at `(view.cx, view.cy)` with uniform +/// `view.scale` dots-per-unit. /// /// Returns `(sx, sy)` as `i32` — suitable for `draw::dot_i`. #[inline] -fn project(x: f32, y: f32, z: f32, ax: f32, ay: f32, cx: i32, cy: i32, scale: f32) -> (i32, i32) { +fn project(x: f32, y: f32, z: f32, view: &View) -> (i32, i32) { // Rotate about X axis. - let (sax, cax) = ax.sin_cos(); + let (sax, cax) = view.ax.sin_cos(); let y1 = y * cax - z * sax; let z1 = y * sax + z * cax; // Rotate about Y axis. - let (say, cay) = ay.sin_cos(); + let (say, cay) = view.ay.sin_cos(); let x2 = x * cay + z1 * say; let y2 = y1; // Orthographic projection (drop z2, flip y for screen coords). - let sx = cx + (x2 * scale).round() as i32; - let sy = cy - (y2 * scale).round() as i32; + let sx = view.cx + (x2 * view.scale).round() as i32; + let sy = view.cy - (y2 * view.scale).round() as i32; (sx, sy) } /// Plot a parametric curve segment from `t0` to `t1` (in `[0, 2π]` or /// `[0, 1]`), sampling `steps` evenly-spaced points, using `f(t) -> (x,y,z)`. -/// Each point is rotated and projected with the given angles / centre / scale. +/// Each point is rotated and projected through `view`. #[inline] -fn plot_curve( - grid: &mut BrailleGrid, - t0: f32, - t1: f32, - steps: usize, - ax: f32, - ay: f32, - cx: i32, - cy: i32, - scale: f32, - f: F, -) where +fn plot_curve(grid: &mut BrailleGrid, t0: f32, t1: f32, steps: usize, view: &View, f: F) +where F: Fn(f32) -> (f32, f32, f32), { if steps == 0 { @@ -79,7 +80,7 @@ fn plot_curve( for i in 0..=steps { let t = t0 + (t1 - t0) * (i as f32 / steps as f32); let (x, y, z) = f(t); - let (sx, sy) = project(x, y, z, ax, ay, cx, cy, scale); + let (sx, sy) = project(x, y, z, view); draw::dot_i(grid, sx, sy); } } @@ -116,13 +117,20 @@ impl ProgressStyle for MobiusStrip { let (cx, cy, scale) = grid_cxys(grid); let ax = ctx.time * 0.37; let ay = ctx.time * 0.53; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; // Reveal u from 0 to eased·2π across ~20 stripes in v. let u_max = ctx.eased * 2.0 * PI; let u_steps = 120usize; let v_lines = 9usize; // v ∈ [-1, 1] in v_lines steps for vi in 0..v_lines { let v = -1.0 + 2.0 * vi as f32 / (v_lines - 1).max(1) as f32; - plot_curve(grid, 0.0, u_max, u_steps, ax, ay, cx, cy, scale, |u| { + plot_curve(grid, 0.0, u_max, u_steps, &view, |u| { let half = 0.5 * v * (u / 2.0).cos(); let x = (1.0 + half) * u.cos(); let y = (1.0 + half) * u.sin(); @@ -154,6 +162,13 @@ impl ProgressStyle for TorusWireframe { let (cx, cy, scale) = grid_cxys(grid); let ax = ctx.time * 0.4 + 0.4; let ay = ctx.time * 0.6; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; let r_big = 1.0_f32; let r_small = 0.38_f32; let n_lat = 14usize; // latitude circles (constant v) @@ -165,7 +180,7 @@ impl ProgressStyle for TorusWireframe { for i in 0..n_lit.min(n_lat) { let v = 2.0 * PI * i as f32 / n_lat as f32; - plot_curve(grid, 0.0, 2.0 * PI, u_steps, ax, ay, cx, cy, scale, |u| { + plot_curve(grid, 0.0, 2.0 * PI, u_steps, &view, |u| { let x = (r_big + r_small * v.cos()) * u.cos(); let y = (r_big + r_small * v.cos()) * u.sin(); let z = r_small * v.sin(); @@ -176,7 +191,7 @@ impl ProgressStyle for TorusWireframe { let n_lon_lit = n_lit.saturating_sub(n_lat).min(n_lon); for i in 0..n_lon_lit { let u = 2.0 * PI * i as f32 / n_lon as f32; - plot_curve(grid, 0.0, 2.0 * PI, u_steps, ax, ay, cx, cy, scale, |v| { + plot_curve(grid, 0.0, 2.0 * PI, u_steps, &view, |v| { let x = (r_big + r_small * v.cos()) * u.cos(); let y = (r_big + r_small * v.cos()) * u.sin(); let z = r_small * v.sin(); @@ -207,6 +222,13 @@ impl ProgressStyle for TorusKnot { let (cx, cy, scale) = grid_cxys(grid); let ax = ctx.time * 0.29; let ay = ctx.time * 0.47; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; let p = 2_i32; let q = 3_i32; let r_big = 1.0_f32; @@ -214,7 +236,7 @@ impl ProgressStyle for TorusKnot { // Parametric: t ∈ [0, 2π·lcm(p,q)] but one pass = 2π suffices for (2,3). let t_max = ctx.eased * 2.0 * PI; let steps = 200usize; - plot_curve(grid, 0.0, t_max, steps, ax, ay, cx, cy, scale, |t| { + plot_curve(grid, 0.0, t_max, steps, &view, |t| { let u = p as f32 * t; let v = q as f32 * t; let x = (r_big + r_small * v.cos()) * u.cos(); @@ -250,7 +272,14 @@ impl ProgressStyle for TrefoilKnot { let steps = 200usize; // Normalise scale: trefoil radius ≈ 3, so shrink by 1/3. let s = scale / 3.0; - plot_curve(grid, 0.0, t_max, steps, ax, ay, cx, cy, s, |t| { + let view = View { + ax, + ay, + cx, + cy, + scale: s, + }; + plot_curve(grid, 0.0, t_max, steps, &view, |t| { let x = t.sin() + 2.0 * (2.0 * t).sin(); let y = t.cos() - 2.0 * (2.0 * t).cos(); let z = -(3.0 * t).sin(); @@ -286,9 +315,16 @@ impl ProgressStyle for KleinBottle { let u_lines = 20usize; let v_steps = 60usize; let s = scale / 2.5; + let view = View { + ax, + ay, + cx, + cy, + scale: s, + }; for ui in 0..u_lines { let u = u_max * ui as f32 / u_lines.max(1) as f32; - plot_curve(grid, 0.0, 2.0 * PI, v_steps, ax, ay, cx, cy, s, |v| { + plot_curve(grid, 0.0, 2.0 * PI, v_steps, &view, |v| { // Standard figure-8 Klein bottle parametrisation. let cu = u.cos(); let su = u.sin(); @@ -337,6 +373,13 @@ impl ProgressStyle for HopfFibers { let t_max = ctx.eased * 2.0 * PI; let steps = 100usize; let s = scale * 0.55; + let view = View { + ax, + ay, + cx, + cy, + scale: s, + }; for &(theta, phi) in &bases { // Hopf fiber: quaternion (cos α, sin α · p) where p ∈ S², // stereographically projected from S³ \ {north pole} to ℝ³. @@ -349,7 +392,7 @@ impl ProgressStyle for HopfFibers { // sin t · cos(θ/2), sin t · sin(θ/2)·e^{iφ}) [simplified] let hth = theta / 2.0; let (shth, chth) = hth.sin_cos(); - plot_curve(grid, 0.0, t_max, steps, ax, ay, cx, cy, s, |t| { + plot_curve(grid, 0.0, t_max, steps, &view, |t| { // q = (q0,q1,q2,q3) ∈ S³. let q0 = t.cos() * chth; let q1 = t.cos() * shth * cp; @@ -393,6 +436,13 @@ impl ProgressStyle for SphereInflate { let (cx, cy, scale) = grid_cxys(grid); let ax = ctx.time * 0.35; let ay = ctx.time * 0.62; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; let r = ctx.eased; // radius grows with eased, 0 → 1 let n_lat = 7usize; let n_lon = 8usize; @@ -402,7 +452,7 @@ impl ProgressStyle for SphereInflate { let theta = PI * (i + 1) as f32 / (n_lat + 1) as f32; let ring_r = r * theta.sin(); let z0 = r * theta.cos(); - plot_curve(grid, 0.0, 2.0 * PI, steps, ax, ay, cx, cy, scale, |phi| { + plot_curve(grid, 0.0, 2.0 * PI, steps, &view, |phi| { (ring_r * phi.cos(), ring_r * phi.sin(), z0) }); } @@ -410,7 +460,7 @@ impl ProgressStyle for SphereInflate { for i in 0..n_lon { let phi = 2.0 * PI * i as f32 / n_lon as f32; let (sp, cp) = phi.sin_cos(); - plot_curve(grid, 0.0, PI, steps, ax, ay, cx, cy, scale, |theta| { + plot_curve(grid, 0.0, PI, steps, &view, |theta| { (r * theta.sin() * cp, r * theta.sin() * sp, r * theta.cos()) }); } @@ -438,18 +488,25 @@ impl ProgressStyle for HelixClimb { let (cx, cy, scale) = grid_cxys(grid); let ax = ctx.time * 0.25 + 0.3; let ay = ctx.time * 0.58; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; let turns = 4.0_f32; let t_max = ctx.eased * turns * 2.0 * PI; let steps = 200usize; let r = 0.6_f32; let height = 2.0_f32; // total height -1 to +1 // Strand A. - plot_curve(grid, 0.0, t_max, steps, ax, ay, cx, cy, scale, |t| { + plot_curve(grid, 0.0, t_max, steps, &view, |t| { let z = -1.0 + height * t / (turns * 2.0 * PI); (r * t.cos(), r * t.sin(), z) }); // Strand B (π offset in phase). - plot_curve(grid, 0.0, t_max, steps, ax, ay, cx, cy, scale, |t| { + plot_curve(grid, 0.0, t_max, steps, &view, |t| { let z = -1.0 + height * t / (turns * 2.0 * PI); (r * (t + PI).cos(), r * (t + PI).sin(), z) }); @@ -458,7 +515,7 @@ impl ProgressStyle for HelixClimb { for i in 0..n_links { let t_link = i as f32 * PI; let z = -1.0 + height * t_link / (turns * 2.0 * PI); - plot_curve(grid, 0.0, 1.0, 4, ax, ay, cx, cy, scale, |s| { + plot_curve(grid, 0.0, 1.0, 4, &view, |s| { let angle_a = t_link; let angle_b = t_link + PI; let xa = r * angle_a.cos(); @@ -492,23 +549,26 @@ impl ProgressStyle for SaddleSurface { let (cx, cy, scale) = grid_cxys(grid); let ax = ctx.time * 0.22 + 0.5; let ay = ctx.time * 0.48; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; let n_lines = 12usize; let revealed = (ctx.eased * n_lines as f32 * 2.0).round() as usize; let steps = 60usize; // x-parallel iso-lines (vary y, constant x). for i in 0..revealed.min(n_lines) { let x = -1.0 + 2.0 * i as f32 / (n_lines - 1).max(1) as f32; - plot_curve(grid, -1.0, 1.0, steps, ax, ay, cx, cy, scale, |y| { - (x, y, x * x - y * y) - }); + plot_curve(grid, -1.0, 1.0, steps, &view, |y| (x, y, x * x - y * y)); } // y-parallel iso-lines (vary x, constant y). let extra = revealed.saturating_sub(n_lines); for i in 0..extra.min(n_lines) { let y = -1.0 + 2.0 * i as f32 / (n_lines - 1).max(1) as f32; - plot_curve(grid, -1.0, 1.0, steps, ax, ay, cx, cy, scale, |x| { - (x, y, x * x - y * y) - }); + plot_curve(grid, -1.0, 1.0, steps, &view, |x| (x, y, x * x - y * y)); } Ok(()) } @@ -539,11 +599,11 @@ impl ProgressStyle for TesseractSpin { // The 16 vertices of a unit tesseract in (x,y,z,w) ∈ {-1,+1}^4. let verts: [[f32; 4]; 16] = { let mut v = [[0.0f32; 4]; 16]; - for i in 0..16usize { - v[i][0] = if i & 1 != 0 { 1.0 } else { -1.0 }; - v[i][1] = if i & 2 != 0 { 1.0 } else { -1.0 }; - v[i][2] = if i & 4 != 0 { 1.0 } else { -1.0 }; - v[i][3] = if i & 8 != 0 { 1.0 } else { -1.0 }; + for (i, vert) in v.iter_mut().enumerate() { + vert[0] = if i & 1 != 0 { 1.0 } else { -1.0 }; + vert[1] = if i & 2 != 0 { 1.0 } else { -1.0 }; + vert[2] = if i & 4 != 0 { 1.0 } else { -1.0 }; + vert[3] = if i & 8 != 0 { 1.0 } else { -1.0 }; } v }; @@ -551,7 +611,7 @@ impl ProgressStyle for TesseractSpin { let mut edges: Vec<(usize, usize)> = Vec::with_capacity(32); for i in 0..16usize { for j in (i + 1)..16usize { - if (i ^ j).count_ones() == 1 { + if (i ^ j).is_power_of_two() { edges.push((i, j)); } } @@ -656,11 +716,18 @@ impl ProgressStyle for RomanSurface { let u_lines = 18usize; let v_steps = 60usize; let s = scale * 0.6; + let view = View { + ax, + ay, + cx, + cy, + scale: s, + }; for ui in 0..u_lines { let u = u_max * ui as f32 / u_lines.max(1) as f32; let (su, cu) = u.sin_cos(); let s2u = (2.0 * u).sin(); - plot_curve(grid, 0.0, PI, v_steps, ax, ay, cx, cy, s, |v| { + plot_curve(grid, 0.0, PI, v_steps, &view, |v| { let (sv, _cv) = v.sin_cos(); let s2v = (2.0 * v).sin(); // Roman surface: x=sin²u·sin 2v, y=sin 2u·sin²v, z=sin 2u·sin 2v / 2. @@ -695,6 +762,13 @@ impl ProgressStyle for SeifertRamp { let (cx, cy, scale) = grid_cxys(grid); let ax = ctx.time * 0.30 + 0.5; let ay = ctx.time * 0.50; + let view = View { + ax, + ay, + cx, + cy, + scale, + }; // Seifert surface for trefoil: parametrised by (r, theta) in a disk, // lifted to 3-D via the trefoil fibration. // Approximation: r ∈ [0,1], theta ∈ [0, 2π]. @@ -706,29 +780,18 @@ impl ProgressStyle for SeifertRamp { // Concentric rings. for ri in 1..=n_radii { let r = ri as f32 / n_radii as f32; - plot_curve( - grid, - 0.0, - theta_max, - steps, - ax, - ay, - cx, - cy, - scale, - |theta| { - let x = r * theta.cos(); - let y = r * theta.sin(); - let z = r * (theta / 3.0).sin() * (1.0 - r * 0.5); - (x, y, z) - }, - ); + plot_curve(grid, 0.0, theta_max, steps, &view, |theta| { + let x = r * theta.cos(); + let y = r * theta.sin(); + let z = r * (theta / 3.0).sin() * (1.0 - r * 0.5); + (x, y, z) + }); } // Radial spokes. let n_spokes = 12usize; for si in 0..n_spokes { let theta = theta_max * si as f32 / n_spokes.max(1) as f32; - plot_curve(grid, 0.0, 1.0, 20, ax, ay, cx, cy, scale, |r| { + plot_curve(grid, 0.0, 1.0, 20, &view, |r| { let x = r * theta.cos(); let y = r * theta.sin(); let z = r * (theta / 3.0).sin() * (1.0 - r * 0.5); diff --git a/src/progress/styles/transit.rs b/src/progress/styles/transit.rs index 9334090..4f41786 100644 --- a/src/progress/styles/transit.rs +++ b/src/progress/styles/transit.rs @@ -498,7 +498,7 @@ impl ProgressStyle for SubwayMap { } // ── Stations ── - let n_stations = ((dw / 8).max(3)).min(16); + let n_stations = (dw / 8).clamp(3, 16); let station_r = (dh as i32 / 6).max(1); for s in 0..n_stations { @@ -1206,7 +1206,7 @@ impl ProgressStyle for FerryCrossing { (wake_y0 + wake_angle_dots).min(dh as i32 - 1), ); // Ripple dots along wake - for w in (0..wake_len as usize).step_by(4.max(1)) { + for w in (0..wake_len as usize).step_by(4) { let phase = w as f32 / wake_len as f32; let spread = (phase * wake_angle_dots as f32) as i32; let wx = stern_x - w as i32; diff --git a/src/progress/styles/weather.rs b/src/progress/styles/weather.rs index 4c14c23..5d75dbe 100644 --- a/src/progress/styles/weather.rs +++ b/src/progress/styles/weather.rs @@ -380,7 +380,7 @@ impl ProgressStyle for RainbowArc { Color::rgb(255, 0, 0), // red (outermost) ]; - for band in 0..bands { + for (band, &color) in band_colors.iter().enumerate() { // Band `band` becomes visible when progress crosses its threshold. let threshold = band as f32 / bands as f32; if ctx.eased < threshold { @@ -407,7 +407,6 @@ impl ProgressStyle for RainbowArc { } // Apply band color. - let color = band_colors[band]; let (cw, ch) = grid.dimensions(); for cy_c in 0..ch { draw::tint_row(grid, cy_c, 0, cw.saturating_sub(1), color); @@ -832,7 +831,7 @@ impl ProgressStyle for FrostCrystals { // Each edge has a set of roots spaced every 4 dots along the edge. // From each root, a main spine grows inward; then side branches sprout // every few dots, each in the perpendicular direction. - let branch_spacing = 4usize.max(1); + let branch_spacing = 4usize; let branch_len_frac = 0.4_f32; // Left edge. diff --git a/src/progress/styles/wildlife.rs b/src/progress/styles/wildlife.rs index 52f9880..e25f6c9 100644 --- a/src/progress/styles/wildlife.rs +++ b/src/progress/styles/wildlife.rs @@ -170,7 +170,7 @@ impl ProgressStyle for GallopingHorse { draw::dot_i(grid, lx, knee_y); // Lower leg + hoof. draw::dot_i(grid, lx, foot_y); - if foot_y + 1 <= base as i32 { + if foot_y < base as i32 { draw::dot_i(grid, lx, foot_y + 1); // hoof ground touch } } @@ -836,7 +836,7 @@ impl ProgressStyle for Murmuration { // Flock: a cloud of dots with per-dot sine offsets. // We generate a deterministic but chaotic-looking distribution. - let n_birds = ((front_x / 3).max(1)).min(60); + let n_birds = (front_x / 3).clamp(1, 60); for b in 0..n_birds { // Each bird has a unique phase and speed. diff --git a/src/progress/styles/wipe.rs b/src/progress/styles/wipe.rs index def1eb2..23b0c6d 100644 --- a/src/progress/styles/wipe.rs +++ b/src/progress/styles/wipe.rs @@ -358,8 +358,8 @@ impl ProgressStyle for Checkerboard { let (dw, dh) = draw::dot_dims(grid); // Tile size: 4 dots wide × 4 dots tall (a 2×1 cell block — distinct from // the Bayer-dither which operates per dot with a 4×4 matrix). - let tile_w = 4usize.max(1); - let tile_h = 4usize.max(1); + let tile_w = 4usize; + let tile_h = 4usize; // Each tile has a phase in [0, 1): even tiles have phase 0, odd 0.5. // A tile is revealed when ctx.eased > phase. for ty in 0.. { @@ -418,7 +418,7 @@ impl ProgressStyle for VenetianBlinds { fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> { let (dw, dh) = draw::dot_dims(grid); // Slat height in dots — 8 dots = 2 cell rows. - let slat_h = 8usize.max(1); + let slat_h = 8usize; let num_slats = (dh + slat_h - 1) / slat_h; for s in 0..num_slats { let top = s * slat_h; @@ -433,7 +433,7 @@ impl ProgressStyle for VenetianBlinds { } // Tint alternating slats. let (cw, ch) = grid.dimensions(); - let slat_c = 2usize.max(1); + let slat_c = 2usize; for cy_idx in 0..ch { let slat_idx = cy_idx / slat_c; let t = if slat_idx % 2 == 0 { 0.25 } else { 0.75 }; @@ -756,8 +756,8 @@ impl ProgressStyle for Pixelate { fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> { let (cw, ch) = grid.dimensions(); // Block size in cells (2 wide × 1 tall — avoids conflating with Bayer). - let block_w = 2usize.max(1); - let block_h = 1usize.max(1); + let block_w = 2usize; + let block_h = 1; // Each block has a pseudorandom order based on its grid position so that // blocks light up in a spatial-hash order rather than simple scan order. // We use a cheap integer hash of (block_col, block_row). diff --git a/src/progress/styles/yantra.rs b/src/progress/styles/yantra.rs index c89c796..8489e44 100644 --- a/src/progress/styles/yantra.rs +++ b/src/progress/styles/yantra.rs @@ -166,7 +166,7 @@ fn arc(grid: &mut BrailleGrid, cx: f32, cy: f32, r: f32, a_start: f32, a_end: f3 return; } let span = (a_end - a_start).abs(); - let steps = ((r * span).ceil() as usize).max(4).min(1024); + let steps = ((r * span).ceil() as usize).clamp(4, 1024); let mut prev: Option<(i32, i32)> = None; for i in 0..=steps { let t = i as f32 / steps as f32; @@ -262,7 +262,7 @@ impl ProgressStyle for SriYantra { } // Draw outer lotus ring (8 petals as arcs) - if reveal >= total + 1 { + if reveal > total { let petal_r = r * 0.18; let ring_r = r * 1.05; let n_petals = 8usize; @@ -655,7 +655,7 @@ impl ProgressStyle for Rangoli { // Grid of pulli dots arranged in concentric diamond rings. // Each ring k has 4k dots at distance k*step from center. let step = (r_max / 4.0).max(2.0); - let n_rings = ((r_max / step) as usize).max(1).min(4); + let n_rings = ((r_max / step) as usize).clamp(1, 4); let total_dots = 1 + (1..=n_rings).map(|k| 4 * k).sum::(); let reveal = (ctx.eased * (total_dots + n_rings * 4) as f32).round() as usize; @@ -940,7 +940,7 @@ impl ProgressStyle for VesicaRosette { let reveal = (ctx.eased * (n_petals + 2) as f32).round() as usize; // Outer enclosing circle - if reveal >= n_petals + 1 { + if reveal > n_petals { circle(grid, cx, cy, r * 1.00); } diff --git a/src/quick.rs b/src/quick.rs index 66a3b8b..f3fd586 100644 --- a/src/quick.rs +++ b/src/quick.rs @@ -102,9 +102,9 @@ const DEFAULT_HEIGHT: usize = 24; /// Tuple of `(width, height)` in terminal cells. #[inline] fn terminal_size() -> (usize, usize) { - crossterm::terminal::size() - .map(|(w, h)| (w as usize, h as usize)) - .unwrap_or((DEFAULT_WIDTH, DEFAULT_HEIGHT)) + crossterm::terminal::size().map_or((DEFAULT_WIDTH, DEFAULT_HEIGHT), |(w, h)| { + (w as usize, h as usize) + }) } /// Waits for any keypress. @@ -858,12 +858,11 @@ fn play_webcam_internal(mut player: crate::media::WebcamPlayer) -> Result<()> { // Check for events with short timeout if event::poll(Duration::from_millis(5))? { match event::read()? { - Event::Key(key_event) => { + Event::Key(key_event) // Stop on any key (except modifiers alone) - if !matches!(key_event.code, KeyCode::Modifier(_)) { + if !matches!(key_event.code, KeyCode::Modifier(_)) => { return Ok(()); } - } Event::Resize(w, h) => { // Handle terminal resize player.handle_resize(w as usize, h as usize); @@ -1082,7 +1081,9 @@ mod tests { assert!(grid.width() > 0); assert!(grid.height() > 0); } - _ => panic!("Expected Static variant for PNG"), + crate::media::MediaContent::Animated(_) => { + panic!("Expected Static variant for PNG") + } } } } diff --git a/src/raytracer/braille.rs b/src/raytracer/braille.rs index 0ee112a..0cee4e2 100644 --- a/src/raytracer/braille.rs +++ b/src/raytracer/braille.rs @@ -1,6 +1,6 @@ //! Braille conversion and green ANSI styling (VIZ-012) -/// Map a block intensity [0,1] to a Braille character U+2800..U+28FF. +/// Map a block intensity `[0, 1]` to a Braille character U+2800..U+28FF. /// We approximate brightness by dot density; this is not a spatial 2x4 sampling. pub fn intensity_to_braille_char(intensity: f32) -> char { let i = intensity.clamp(0.0, 1.0); @@ -58,9 +58,9 @@ pub fn intensity_buffer_to_green_braille(buffer: &[Vec]) -> String { for y in (0..height).step_by(step_y) { for x in (0..width).step_by(step_x) { let mut max_i = 0.0_f32; - for yy in y..(y + step_y).min(height) { - for xx in x..(x + step_x).min(width) { - max_i = max_i.max(buffer[yy][xx]); + for row in buffer.iter().take((y + step_y).min(height)).skip(y) { + for &v in row.iter().take((x + step_x).min(width)).skip(x) { + max_i = max_i.max(v); } } let ch = intensity_to_braille_char(max_i); diff --git a/src/raytracer/gltf_loader.rs b/src/raytracer/gltf_loader.rs index 762979c..e584435 100644 --- a/src/raytracer/gltf_loader.rs +++ b/src/raytracer/gltf_loader.rs @@ -85,6 +85,7 @@ impl MeshData { /// Normalize the mesh to fit within a unit cube centered at origin /// Returns a new MeshData with transformed positions + #[must_use] pub fn normalize(&self) -> Self { let center = self.center(); let max_dim = self.max_dimension(); @@ -131,6 +132,12 @@ impl MeshData { /// * `Ok(MeshData)` - Successfully loaded mesh data /// * `Err(...)` - File not found, invalid format, or missing required data /// +/// # Errors +/// Returns an error if the file cannot be read or parsed as glTF, if it contains +/// no mesh or no primitive, if the primitive lacks position or index data, if the +/// normal count differs from the position count, if the index count is not a +/// multiple of 3, or if any index is out of range for the vertex list. +/// /// # Example /// ```ignore /// use dotmax::raytracer::gltf_loader::load_gltf; @@ -205,8 +212,8 @@ pub fn load_gltf(path: &str) -> Result { ); } - // Validate all indices are in bounds - let max_index = *indices.iter().max().unwrap(); + // Validate all indices are in bounds (indices is non-empty, checked above) + let max_index = indices.iter().copied().max().unwrap_or(0); if max_index >= positions.len() as u32 { anyhow::bail!( "Index out of bounds: {} >= {} vertices", diff --git a/src/raytracer/lighting.rs b/src/raytracer/lighting.rs index 68afd54..7140849 100644 --- a/src/raytracer/lighting.rs +++ b/src/raytracer/lighting.rs @@ -25,6 +25,10 @@ pub fn calculate_diffuse_shading(point: Vector3, normal: Vector3, light: &Light) } #[cfg(test)] +// Exact float equality is the property under test: these assert exact stored/reset +// values (a 0.5 stop, 0.0 fps after reset, exactly-zero shading for a perpendicular +// light). All are exactly representable, so an epsilon compare would weaken them. +#[allow(clippy::float_cmp)] mod tests { use super::*; diff --git a/src/raytracer/math.rs b/src/raytracer/math.rs index 27577ce..8098049 100644 --- a/src/raytracer/math.rs +++ b/src/raytracer/math.rs @@ -40,6 +40,7 @@ impl Vector3 { } #[inline] + #[must_use] pub fn cross(&self, other: &Vector3) -> Vector3 { Vector3::new( self.y * other.z - self.z * other.y, @@ -49,6 +50,7 @@ impl Vector3 { } #[inline] + #[must_use] pub fn normalize(&self) -> Vector3 { let len = self.length(); if len == 0.0 { diff --git a/src/raytracer/obj_loader.rs b/src/raytracer/obj_loader.rs index 405ab03..b64019b 100644 --- a/src/raytracer/obj_loader.rs +++ b/src/raytracer/obj_loader.rs @@ -20,7 +20,7 @@ fn parse_index(tok: &str, len: usize) -> Option { } // negative let n = len as i32; - idx = n + idx; // idx is negative + idx += n; // idx is negative if idx >= 0 { return Some(idx as usize); } @@ -29,6 +29,11 @@ fn parse_index(tok: &str, len: usize) -> Option { } /// Load a very simple OBJ (triangles/quads) into MeshData +/// +/// # Errors +/// +/// Returns an error if the file cannot be opened, or if it contains no +/// geometry (no vertex positions, or no faces referencing them). pub fn load_obj>(path: P) -> Result { let file = File::open(&path) .with_context(|| format!("Failed to open OBJ file: {}", path.as_ref().display()))?; @@ -71,10 +76,10 @@ pub fn load_obj>(path: P) -> Result { let z: f32 = parts[3].parse().unwrap_or(0.0); normals.push(Vector3::new(x, y, z)); } - } else if s.starts_with("f ") { + } else if let Some(face_spec) = s.strip_prefix("f ") { // faces: triangulate fan let mut face_verts: Vec = Vec::new(); - for tok in s[2..].split_whitespace() { + for tok in face_spec.split_whitespace() { // formats: v, v//vn, v/vt, v/vt/vn let mut v_idx: Option = None; let mut vn_idx: Option = None; diff --git a/src/raytracer/renderer.rs b/src/raytracer/renderer.rs index b31be03..0a41cab 100644 --- a/src/raytracer/renderer.rs +++ b/src/raytracer/renderer.rs @@ -32,8 +32,8 @@ pub fn render_with_orientation( ) -> Vec> { let mut buffer = vec![vec![0.0_f32; width]; height]; - for y in 0..height { - for x in 0..width { + for (y, row) in buffer.iter_mut().enumerate() { + for (x, pixel) in row.iter_mut().enumerate() { let u = if width > 1 { x as f32 / (width as f32 - 1.0) } else { @@ -46,8 +46,9 @@ pub fn render_with_orientation( }; let ray = camera.get_ray(u, v); - let intensity = if let Some(hit) = scene.hit(&ray, 0.001, f32::MAX) { - match mode { + let intensity = scene + .hit(&ray, 0.001, f32::MAX) + .map_or(0.0, |hit| match mode { RenderMode::Wireframe { step_rad, tol_rad } => { // Apply optional orientation to the normal before grid test if is_on_wireframe_normal_rotated( @@ -69,12 +70,9 @@ pub fn render_with_orientation( } sum.clamp(0.0, 1.0) } - } - } else { - 0.0 - }; + }); - buffer[y][x] = intensity; + *pixel = intensity; } } @@ -82,6 +80,9 @@ pub fn render_with_orientation( } /// Simple edge/vertex renderer for mesh scenes with hidden-line removal. +// Public API re-exported from `raytracer::mod`; grouping the parameters into a +// struct would be a breaking change for downstream callers. +#[allow(clippy::too_many_arguments)] pub fn render_edges_with_orientation( scene: &Scene, camera: &Camera, @@ -203,7 +204,7 @@ pub fn render_edges_with_orientation( // Draw vertices on top (will only appear if nearest at that pixel) let vert_r = (vertex_px.max(1) / 2).max(0); - for &p in verts.iter() { + for &p in verts { if let Some((x, y, d)) = project(p) { draw_disc(x, y, vert_r, d, &mut set_px_depth); } diff --git a/src/raytracer/scene.rs b/src/raytracer/scene.rs index e8804b1..6e5bfb4 100644 --- a/src/raytracer/scene.rs +++ b/src/raytracer/scene.rs @@ -17,6 +17,12 @@ pub struct Scene { pub(crate) mesh_edges: Option>, } +impl Default for Scene { + fn default() -> Self { + Self::new() + } +} + impl Scene { pub fn new() -> Self { Self { @@ -62,6 +68,10 @@ impl Scene { /// * `Ok(Scene)` - Scene with loaded model and light /// * `Err(...)` - Failed to load model /// + /// # Errors + /// Returns an error if the glTF/GLB file cannot be loaded (see [`load_gltf`]): + /// missing or unparseable file, no mesh data, or invalid geometry. + /// /// # Example /// ```no_run /// use dotmax::raytracer::Scene; @@ -71,17 +81,20 @@ impl Scene { pub fn new_with_model(path: &str) -> Result { // Load mesh data from glTF file let mesh_data = load_gltf(path)?; - Self::from_mesh_data(mesh_data) + Ok(Self::from_mesh_data(mesh_data)) } /// Create a scene with an OBJ model loaded from file (simple importer) + /// + /// # Errors + /// Returns an error if the OBJ file cannot be read or parsed (see [`load_obj`]). pub fn new_with_obj_model(path: &str) -> Result { let data = load_obj(path)?; - Self::from_mesh_data(data) + Ok(Self::from_mesh_data(data)) } /// Common builder from MeshData: normalize, build mesh, add default light - fn from_mesh_data(mesh_data: MeshData) -> Result { + fn from_mesh_data(mesh_data: MeshData) -> Self { // Normalize the mesh to fit in a standard view (centered at origin, scaled to ~2 units) let normalized_data = mesh_data.normalize(); @@ -134,12 +147,12 @@ impl Scene { // Position it above and to the side for good visibility let light = Light::new(Vector3::new(2.0, 2.0, 2.0), 1.0); - Ok(Self { + Self { objects: vec![mesh], lights: vec![light], mesh_vertices: Some(positions), mesh_edges: Some(edges), - }) + } } /// If the scene was created from a mesh, returns cached vertex positions diff --git a/src/raytracer/wireframe.rs b/src/raytracer/wireframe.rs index 77edf48..8658d7e 100644 --- a/src/raytracer/wireframe.rs +++ b/src/raytracer/wireframe.rs @@ -3,7 +3,8 @@ use super::math::Vector3; /// Default wireframe grid spacing (radians) and tolerance (thickness in radians) -pub const DEFAULT_WIREFRAME_STEP_RAD: f32 = 10.0_f32.to_radians(); +// `f32::to_radians` is not const-stable until Rust 1.85; MSRV is 1.70. 10 degrees == PI / 18. +pub const DEFAULT_WIREFRAME_STEP_RAD: f32 = std::f32::consts::PI / 18.0; pub const DEFAULT_WIREFRAME_TOL_RAD: f32 = 0.03; // ~1.7 degrees /// Returns true if the surface normal lies on a wireframe grid line. diff --git a/tests/image_rendering_tests.rs b/tests/image_rendering_tests.rs index 36e2d18..9226704 100644 --- a/tests/image_rendering_tests.rs +++ b/tests/image_rendering_tests.rs @@ -11,6 +11,28 @@ mod image_pipeline_tests { }; use std::path::Path; + /// Build an RGB image of the given size in memory. + /// + /// The extreme-size cases below used to load `viper_4k.png`, `viper_ultra_tall.png` + /// and `viper_ultra_wide.png` — ~31 MB of fixtures that are gitignored. That meant + /// the tests only passed on a machine which already had the files; on a fresh clone + /// (and in CI under `--all-features`) they failed on a missing file. Synthesizing + /// the pixels keeps the dimensions and the pipeline under test while making the + /// tests self-contained. The value varies per pixel so `auto_threshold` sees a real + /// distribution rather than a flat image. + fn synthetic_rgb(width: u32, height: u32) -> image::DynamicImage { + let mut raw = vec![0u8; width as usize * height as usize * 3]; + for (i, px) in raw.chunks_exact_mut(3).enumerate() { + let v = (i % 251) as u8; + px[0] = v; + px[1] = v ^ 0x55; + px[2] = v ^ 0xAA; + } + let buf = + image::RgbImage::from_raw(width, height, raw).expect("raw buffer sized correctly"); + image::DynamicImage::ImageRgb8(buf) + } + #[test] fn test_full_pipeline_with_threshold() { // Load test image @@ -190,9 +212,8 @@ mod image_pipeline_tests { // Story 3.5.5: Integration tests for extreme aspect ratio images #[test] fn test_extreme_wide_aspect_ratio_renders_successfully() { - // Test with viper_ultra_wide.png (10000×4000, 2.5:1 aspect ratio) - let img_path = Path::new("tests/fixtures/images/viper_ultra_wide.png"); - let img = load_from_path(img_path).expect("Failed to load extreme wide image"); + // 10000×4000, 2.5:1 aspect ratio + let img = synthetic_rgb(10000, 4000); // Verify dimensions assert_eq!(img.width(), 10000); @@ -220,9 +241,8 @@ mod image_pipeline_tests { #[test] fn test_extreme_tall_aspect_ratio_renders_successfully() { - // Test with viper_ultra_tall.png (4000×10000, 1:2.5 aspect ratio) - let img_path = Path::new("tests/fixtures/images/viper_ultra_tall.png"); - let img = load_from_path(img_path).expect("Failed to load extreme tall image"); + // 4000×10000, 1:2.5 aspect ratio + let img = synthetic_rgb(4000, 10000); // Verify dimensions assert_eq!(img.width(), 4000); @@ -276,9 +296,8 @@ mod image_pipeline_tests { #[test] fn test_very_large_square_image_no_regression() { - // Test with viper_4k.png (4000×4000) to ensure normal large images work - let img_path = Path::new("tests/fixtures/images/viper_4k.png"); - let img = load_from_path(img_path).expect("Failed to load large square image"); + // 4000×4000 — ensure normal large images still work + let img = synthetic_rgb(4000, 4000); // Verify dimensions assert_eq!(img.width(), 4000); @@ -679,7 +698,7 @@ mod svg_pipeline_tests { } #[test] - #[ignore] // TODO: SVG background handling issue from Story 3.6 - test fails with 95% black pixels, needs investigation separately from Story 3.5.5 performance optimization + #[ignore = "SVG background handling bug (Story 3.6): renders 95% black pixels; needs separate investigation"] fn test_svg_dark_background_light_content_renders_correctly() { // Regression test for adaptive background bug (2025-11-20) // SVG with dark background (#4d4d4d) and light/white content should render visibly diff --git a/tests/media_integration_tests.rs b/tests/media_integration_tests.rs index 1291b7b..d5e5e28 100644 --- a/tests/media_integration_tests.rs +++ b/tests/media_integration_tests.rs @@ -259,7 +259,6 @@ mod svg_routing_tests { #[cfg(feature = "video")] mod video_routing_tests { - use super::*; use dotmax::media::{detect_format_from_bytes, MediaFormat, VideoCodec}; #[test] diff --git a/tests/property_tests.rs b/tests/property_tests.rs index bdc2404..6ca52c6 100644 --- a/tests/property_tests.rs +++ b/tests/property_tests.rs @@ -517,7 +517,7 @@ mod density_tests { .map(|i| char::from_u32(32 + i as u32).unwrap_or(' ')) .collect(); - let result = DensitySet::new("Custom".to_string(), chars.clone()); + let result = DensitySet::new("Custom".to_string(), chars); prop_assert!(result.is_ok()); let density = result.unwrap(); diff --git a/tests/visual/mod.rs b/tests/visual/mod.rs index 3297d82..953e780 100644 --- a/tests/visual/mod.rs +++ b/tests/visual/mod.rs @@ -28,6 +28,11 @@ //! 2. Review changes in `tests/visual/baselines/` //! 3. Commit new baselines if changes are intentional +// BASELINE_DIR and the two baseline helpers below are maintenance tooling, driven by +// UPDATE_BASELINES=1 when adding or refreshing a visual test. The regression suite +// itself compares captured grids inline, so they are not called from it. +#![allow(dead_code)] + use dotmax::BrailleGrid; use std::fs; use std::path::Path; diff --git a/tests/visual_regression.rs b/tests/visual_regression.rs index 998dbfa..affdc4a 100644 --- a/tests/visual_regression.rs +++ b/tests/visual_regression.rs @@ -35,7 +35,7 @@ use dotmax::{ }, BrailleGrid, }; -use visual::{capture_grid, compare_with_baseline, generate_baseline}; +use visual::capture_grid; // ============================================================================= // Grid Pattern Tests