bevy_pixel_buffer 0.3.0

A library to draw pixels in bevy
Documentation
use bevy::{
    diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin},
    math::{DVec2, DVec4},
    prelude::*,
};
use bevy_egui::{
    egui::{self, RichText},
    EguiContext, EguiPlugin,
};
use bevy_pixel_buffer::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugin(EguiPlugin)
        .add_plugin(FrameTimeDiagnosticsPlugin::default())
        .add_plugin(PixelBufferPlugin)
        .add_startup_system(
            PixelBufferBuilder::new()
                .with_size((1280, 720))
                .with_fill(Fill::window().with_stretch(true))
                .setup(),
        )
        .add_system(process_input)
        .add_system(ui)
        .add_system(render.after(process_input))
        .insert_resource(Params::default())
        .run()
}

fn process_input(mut params: ResMut<Params>, keyboard_input: Res<Input<KeyCode>>, time: Res<Time>) {
    let state = params.as_mut();
    let delta = time.delta().as_secs_f64();
    const MOVE_SPEED: f64 = 0.2;
    if keyboard_input.pressed(KeyCode::A) {
        state.center.x -= state.scale * MOVE_SPEED * delta;
    }
    if keyboard_input.pressed(KeyCode::D) {
        state.center.x += state.scale * MOVE_SPEED * delta;
    }
    if keyboard_input.pressed(KeyCode::W) {
        state.center.y += state.scale * MOVE_SPEED * delta;
    }
    if keyboard_input.pressed(KeyCode::S) {
        state.center.y -= state.scale * MOVE_SPEED * delta;
    }
    const SCALE_SPEED: f64 = 1.1;
    if keyboard_input.pressed(KeyCode::Q) {
        state.scale *= SCALE_SPEED;
    }
    if keyboard_input.pressed(KeyCode::E) {
        state.scale /= SCALE_SPEED;
    }
}

fn ui(
    mut egui_ctx: ResMut<EguiContext>,
    diagnostics: Res<Diagnostics>,
    mut params: ResMut<Params>,
) {
    let params = params.as_mut();
    let fps = diagnostics
        .get(FrameTimeDiagnosticsPlugin::FPS)
        .unwrap()
        .average()
        .unwrap_or_default();

    let ctx = egui_ctx.ctx_mut();
    egui::Window::new("CPU Mandelbrot set visualization").show(ctx, |ui| {
        ui.collapsing(RichText::new("About").heading(), |ui| {
            ui.label(concat!(
                "Mandelbrot set visuzalizer in the CPU. Slower than the ",
                "GPU alternative, but can zoom more because it uses 64 bits of ",
                "precision, currently not supported in WGSL."
            ));
        });
        ui.heading("Controls");
        egui::Grid::new("controls")
            .num_columns(2)
            .striped(true)
            .show(ui, |ui| {
                ui.label("Move");
                ui.label("WASD / Arrows");
                ui.end_row();

                ui.label("Zoom in");
                ui.label("Q");
                ui.end_row();
                ui.label("Zoom out");
                ui.label("E");
                ui.end_row();
            });

        ui.separator();
        ui.horizontal(|ui| {
            ui.label("FPS: ");
            ui.label(RichText::new(format!("{fps:.1}")).code());
        });
        ui.horizontal(|ui| {
            ui.label("Max iterations");
            ui.add(egui::Slider::new(&mut params.max_iter, 64..=2048));
        });
    });
}

fn render(mut pb: QueryPixelBuffer, params: Res<Params>) {
    fn square_complex(c: DVec2) -> DVec2 {
        DVec2::new(c.x * c.x - c.y * c.y, 2.0 * c.x * c.y)
    }

    let mut frame = pb.frame();
    let dimensions = frame.size();
    frame.per_pixel_par(|pos, _| {
        let max_iter = params.max_iter;
        let center = params.center;
        let scale = params.scale;

        let w = dimensions.x as f64;
        let h = dimensions.y as f64;
        let aspect = w / h;

        let x = scale * aspect * (pos.x as f64 - 0.5 * w) / w + center.x;
        let y = -scale * (pos.y as f64 - 0.5 * h) / h + center.y;

        let c = DVec2::new(x, y);
        let mut z = DVec2::ZERO;

        let mut b = -1;
        for i in 0..max_iter {
            if (z.x * z.x + z.y * z.y) > 4.0 {
                b = i;
                break;
            }
            z = square_complex(z) + c;
        }
        if b == -1 {
            b = max_iter;
        }
        let intensity = b as f64 / max_iter as f64;
        let intensity = (2.0 * intensity) / (intensity.abs() + 1.0);
        let r = f64::max(0.0, 2.0 * intensity - 1.0);

        DVec4::new(r, intensity, intensity, 1.0)
    });
}

#[derive(Clone, Debug, Resource)]
struct Params {
    max_iter: i32,
    scale: f64,
    center: DVec2,
}

impl Default for Params {
    fn default() -> Self {
        Self {
            max_iter: 128,
            scale: 1.0,
            center: DVec2::ZERO,
        }
    }
}