From 984492484202e8909bf0f9ec42b9280b72dc008b Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sun, 7 Sep 2025 16:28:35 -0500 Subject: [PATCH] Fill in the rest of the owl (basic impl done) And that's the program, all finished and working as intended. Step 1: boiler plate. Step 2: Everything else. The program isn't that complicated, so I didn't really feel the need to spread it out over several commits. --- Cargo.toml | 5 ++ src/lib.rs | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 ++ 3 files changed, 137 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 61941c9..ec3e807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,8 @@ edition = "2024" [dependencies] bevy = "0.16" +rand = "0.9.2" +rand_chacha = "0.9.0" + +[features] +dynamic_linking = ["bevy/dynamic_linking"] \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8b13789..7d49034 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,130 @@ +use bevy::{ + asset::RenderAssetUsages, + color::palettes::css::*, + prelude::*, + render::render_resource::{Extent3d, TextureDimension, TextureFormat}, +}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +const IMAGE_WIDTH: u32 = 800; +const IMAGE_HEIGHT: u32 = 600; + +pub struct ChaosGamePlugin; + +impl Plugin for ChaosGamePlugin { + fn build(&self, app: &mut App) { + app.insert_resource(ClearColor(BLACK.into())) + .insert_resource(Attractor::triangle()) + .insert_resource(Time::::from_hz(1024.0)) // make it draw really fast + .add_systems(Startup, setup) + .add_systems(FixedUpdate, walk_point); + } +} + +fn setup(mut commands: Commands, mut images: ResMut>, att: Res) { + commands.spawn(Camera2d); + + commands.insert_resource(SeededRng(ChaCha8Rng::seed_from_u64(0))); + + let mut image = Image::new_fill( + Extent3d { + width: IMAGE_WIDTH, + height: IMAGE_HEIGHT, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &(BLACK.to_u8_array()), + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, + ); + + // Paint the target points so they can be seen + for idx in 0..3 { + let point = att.points[idx]; + let color = att.colors[idx]; + let pixel_bytes = image + .pixel_bytes_mut(UVec3::new(point.x as u32, point.y as u32, 0)) + .unwrap(); + fill_color(pixel_bytes, color); + } + + // Spawn a sprite with this image and store the handle in the [`Drawing`] + // resource for lookup during the walk function. + let handle = images.add(image); + commands.spawn(Sprite::from_image(handle.clone())); + commands.insert_resource(Drawing(handle)); + + // Spawn the Walker entity + // TODO: Pull in an RNG and randomly place the walker. + commands.spawn(Walker(Vec2::new(IMAGE_WIDTH as f32 / 2., IMAGE_HEIGHT as f32 / 2.))); +} + +fn walk_point( + drawing: Res, + mut images: ResMut>, + mut walker: Single<&mut Walker>, + att: Res, + mut rng: ResMut, +) { + // Walk towards the next point. Select at random, step half the distance + // between the walker and that point. + let tgt = rng.0.random_range(0..3usize); + let delta_p = (att.points[tgt] - walker.0) / 2.0; + walker.0 = walker.0 + delta_p; + + // Paint the pixel at the new walker position with the color of the chosen + // target. + + let image = images.get_mut(&drawing.0).expect("Image not found"); + fill_color( + image + .pixel_bytes_mut(UVec3::new(walker.0.x as u32, walker.0.y as u32, 0)) + .unwrap(), + att.colors[tgt], + ) +} + +/// Utility function to fill in a pixel by reaching through it's byte slice. +fn fill_color(pixel_slice: &mut [u8], color: Color) { + pixel_slice[0] = (color.to_linear().red * u8::MAX as f32) as u8; + pixel_slice[1] = (color.to_linear().green * u8::MAX as f32) as u8; + pixel_slice[2] = (color.to_linear().blue * u8::MAX as f32) as u8; + pixel_slice[3] = u8::MAX; // hard-code alpha to be fully opaque. + // I'm not dealing with transparency bugs today. +} + +#[derive(Resource)] +struct SeededRng(ChaCha8Rng); + +/// The entity that walks around leaving colored dots along the way. +#[derive(Component)] +pub struct Walker(Vec2); + +#[derive(Resource)] +struct Drawing(Handle); + +/// An "attractor" is the set of points that will be walked between to draw the +/// shape. +/// +/// TODO: Support other shapes. +#[derive(Resource)] +pub struct Attractor { + points: [Vec2; 3], + colors: [Color; 3], +} + +impl Attractor { + /// Create a triangle whose verticies will be used as the target locations. + pub fn triangle() -> Self { + Self { + points: [ + Vec2::new(10., 10.), // bottom left + Vec2::new((IMAGE_WIDTH / 2) as f32, (IMAGE_HEIGHT - 10) as f32), // top middle + Vec2::new((IMAGE_WIDTH - 10) as f32, 10.), // bottom right + ], + colors: [RED.into(), GREEN.into(), BLUE.into()], + } + } +} diff --git a/src/main.rs b/src/main.rs index 200a494..a7fc31a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ use bevy::{prelude::*, winit::WinitSettings}; +use chaos_game_rs::ChaosGamePlugin; + fn main() { App::new() .insert_resource(WinitSettings::desktop_app()) @@ -10,5 +12,6 @@ fn main() { }), ..default() })) + .add_plugins(ChaosGamePlugin) .run(); }