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.
This commit is contained in:
2025-09-07 16:28:35 -05:00
parent 977595ab4d
commit 9844924842
3 changed files with 137 additions and 0 deletions

View File

@@ -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"]

View File

@@ -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::<Fixed>::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<Assets<Image>>, att: Res<Attractor>) {
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<Drawing>,
mut images: ResMut<Assets<Image>>,
mut walker: Single<&mut Walker>,
att: Res<Attractor>,
mut rng: ResMut<SeededRng>,
) {
// 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<Image>);
/// 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()],
}
}
}

View File

@@ -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();
}