summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/camera.rs58
-rw-r--r--src/config.rs3
-rw-r--r--src/main.rs280
-rw-r--r--src/sim/board.rs72
-rw-r--r--src/sim/materials.rs22
-rw-r--r--src/sim/mod.rs4
-rw-r--r--src/sim/overlay.rs50
-rw-r--r--src/sim/sim.rs3
-rw-r--r--src/ui.rs33
9 files changed, 424 insertions, 101 deletions
diff --git a/src/camera.rs b/src/camera.rs
new file mode 100644
index 0000000..695baf5
--- /dev/null
+++ b/src/camera.rs
@@ -0,0 +1,58 @@
+use crate::{
+ config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH},
+ sim::{board::Board, materials::MATERIALS},
+};
+
+pub struct CameraState {
+ // centre coords
+ pub x: f64,
+ pub y: f64,
+ // zoom scale factor, 1.0 = board <-> frame
+ pub zoom: f64,
+}
+
+pub fn screen_position_to_board(
+ board: &Board,
+ camera: &CameraState,
+ screen_x: f64,
+ screen_y: f64,
+) -> (f64, f64) {
+ let camera_width = camera.zoom * f64::from(board.size_x);
+ let camera_height = camera.zoom * f64::from(board.size_y);
+ let camera_start_x = camera.x - camera_width / 2.0;
+ let camera_start_y = camera.y - camera_height / 2.0;
+ (
+ (screen_x / PIXEL_BUFFER_WIDTH as f64 * camera_width) + camera_start_x,
+ (screen_y / PIXEL_BUFFER_HEIGHT as f64 * camera_height) + camera_start_y,
+ )
+}
+
+pub fn write_frame_view(
+ frame: &mut [u8],
+ board: &Board,
+ camera: &CameraState,
+ get_overlay: impl Fn(i32, i32) -> (u8, u8, u8, u8),
+) {
+ for frame_y in 0..PIXEL_BUFFER_HEIGHT {
+ for frame_x in 0..PIXEL_BUFFER_WIDTH {
+ let (x_coord, y_coord) =
+ screen_position_to_board(board, camera, frame_x as f64, frame_y as f64);
+
+ let cell = board.cell_at_position(x_coord as i32, y_coord as i32);
+ let cell_color: Option<(u8, u8, u8, u8)> = cell.map(|c| {
+ let m = MATERIALS[c.material as usize];
+ (m.r, m.g, m.b, 0xFF)
+ });
+ let off_grid_color: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0xFF);
+
+ let target_color: (u8, u8, u8, u8) = cell_color.unwrap_or(off_grid_color);
+ let overlay = get_overlay(x_coord as i32, y_coord as i32);
+
+ let frame_idx = ((frame_y * PIXEL_BUFFER_WIDTH + frame_x) * 4) as usize;
+ frame[frame_idx] = target_color.0.saturating_add(overlay.0);
+ frame[frame_idx + 1] = target_color.1.saturating_add(overlay.1);
+ frame[frame_idx + 2] = target_color.2.saturating_add(overlay.2);
+ frame[frame_idx + 3] = target_color.3.saturating_add(overlay.3);
+ }
+ }
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..75cb581
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,3 @@
+pub const WINDOW_TITLE: &str = "pxs";
+pub const PIXEL_BUFFER_WIDTH: u32 = 320;
+pub const PIXEL_BUFFER_HEIGHT: u32 = 240;
diff --git a/src/main.rs b/src/main.rs
index d818b04..64e57df 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,26 +1,51 @@
+mod camera;
+mod config;
+mod sim;
+mod ui;
+
+use egui::Id;
use egui_wgpu::{RendererOptions, ScreenDescriptor};
use egui_winit::egui::{self, Context};
-use pixels::{Pixels, SurfaceTexture};
+use pixels::{Pixels, ScalingMode, SurfaceTexture};
use std::time::{Duration, Instant};
use winit::{
application::ApplicationHandler,
event::{ElementState, MouseButton, WindowEvent},
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
+ keyboard::Key::{self},
window::Window,
};
-const WIDTH: u32 = 320;
-const HEIGHT: u32 = 240;
-const FPS: u64 = 60;
-const FRAME_DURATION: Duration = Duration::from_micros(1_000_000 / FPS);
+use crate::{
+ camera::{CameraState, screen_position_to_board, write_frame_view},
+ config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH, WINDOW_TITLE},
+ sim::{board::Board, overlay::create_compute_combined_overlay_offset},
+ ui::draw_egui,
+};
pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = std::result::Result<T, Error>;
-struct Clicked {
- x: usize,
- y: usize,
- s: f64,
+struct Config {
+ fps: u16,
+ brush_size: u8,
+}
+
+struct Input {
+ last_mouse_pos_on_screen: (f64, f64),
+ last_mouse_pos_on_board: (i32, i32),
+ is_lmb_down: bool,
+}
+
+struct State {
+ // debug
+ red_level: u8,
+ green_level: u8,
+ blue_level: u8,
+}
+
+struct Diagnostics {
+ fps: f32,
}
struct App {
@@ -30,47 +55,62 @@ struct App {
egui_context: Option<egui::Context>,
window: Option<&'static Window>,
pixels: Option<Pixels<'static>>,
- last_frame: Instant,
- // input
- last_mouse_pos: Option<(f64, f64)>,
+ // input state
+ input: Input,
- // experiemnts
- clicked: Vec<Clicked>,
+ // camera state
+ camera: Option<CameraState>,
- // debug
- red_level: u8,
- green_level: u8,
- blue_level: u8,
-}
+ // game state
+ board: Option<Board>,
-impl App {
- fn set_px(f: &mut [u8], x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) {
- let idx = ((y * WIDTH + x) * 4) as usize;
- f[idx] = r;
- f[idx + 1] = g;
- f[idx + 2] = b;
- f[idx + 3] = a;
- }
+ // used to wait for drawing
+ last_frame_requested: Instant,
+ // used to compute delta_time
+ last_frame_real: Instant,
+
+ config: Config,
+ state: State,
+ diagnostics: Diagnostics,
}
impl Default for App {
fn default() -> Self {
Self {
- egui_context: None,
- egui_state: None,
egui_renderer: None,
+ egui_state: None,
+ egui_context: None,
window: None,
pixels: None,
- last_frame: Instant::now(),
- last_mouse_pos: Some((0.0, 0.0)),
+ input: Input {
+ last_mouse_pos_on_screen: (0.0, 0.0),
+ last_mouse_pos_on_board: (0, 0),
+ is_lmb_down: false,
+ },
- clicked: Vec::new(),
+ camera: Some(CameraState {
+ x: 0.0,
+ y: 0.0,
+ zoom: 0.5,
+ }),
- red_level: 0xFF,
- green_level: 0xFF,
- blue_level: 0xFF,
+ board: Some(Board::empty()),
+
+ last_frame_requested: Instant::now(),
+ last_frame_real: Instant::now(),
+
+ config: Config {
+ fps: 120,
+ brush_size: 10,
+ },
+ state: State {
+ red_level: 0xFF,
+ green_level: 0xFF,
+ blue_level: 0xFF,
+ },
+ diagnostics: Diagnostics { fps: 0.0 },
}
}
}
@@ -78,14 +118,16 @@ impl Default for App {
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = event_loop
- .create_window(Window::default_attributes().with_title("pxs"))
+ .create_window(Window::default_attributes().with_title(WINDOW_TITLE))
.unwrap();
let size = window.inner_size();
let window_ref: &'static Window = Box::leak(Box::new(window));
let surface = SurfaceTexture::new(size.width, size.height, window_ref);
- let pixels = Pixels::new(WIDTH, HEIGHT, surface).unwrap();
+ let mut pixels = Pixels::new(PIXEL_BUFFER_WIDTH, PIXEL_BUFFER_HEIGHT, surface).unwrap();
+
+ pixels.set_scaling_mode(ScalingMode::Fill);
let egui_context = Context::default();
let egui_state = egui_winit::State::new(
@@ -122,27 +164,35 @@ impl ApplicationHandler for App {
&& let Some(egui_context) = &self.egui_context
&& let Some(egui_state) = &mut self.egui_state
&& let Some(egui_renderer) = &mut self.egui_renderer
+ && let Some(board) = &mut self.board
+ && let Some(camera) = &mut self.camera
{
- // TODO how to use the response?
- let _ = egui_state.on_window_event(window, &event);
+ let egui_response = egui_state.on_window_event(window, &event);
+ // if egui consumed the event, it means we shouldn't treat any e.g., mouse clicks
+ if egui_response.consumed {
+ return;
+ }
match event {
+ WindowEvent::KeyboardInput { event, .. } => match event.logical_key {
+ Key::Character(char) => {
+ if char == "z" {
+ camera.zoom += 0.1;
+ }
+ }
+ _ => {}
+ },
WindowEvent::CursorMoved { position, .. } => {
- self.last_mouse_pos = Some((position.x, position.y));
+ self.input.last_mouse_pos_on_screen = (position.x, position.y);
+ self.input.last_mouse_pos_on_board = pixels
+ .window_pos_to_pixel((position.x as f32, position.y as f32))
+ .map(|v| screen_position_to_board(board, camera, v.0 as f64, v.1 as f64))
+ .map(|v| (v.0 as i32, v.1 as i32))
+ .unwrap();
}
WindowEvent::MouseInput { state, button, .. } => {
- if state == ElementState::Pressed
- && button == MouseButton::Left
- && let Some(last_mouse_pos) = self.last_mouse_pos
- {
- let coords = pixels
- .window_pos_to_pixel((last_mouse_pos.0 as f32, last_mouse_pos.1 as f32))
- .unwrap();
- self.clicked.push(Clicked {
- x: coords.0,
- y: coords.1,
- s: 25.0,
- });
+ if button == MouseButton::Left {
+ self.input.is_lmb_down = state == ElementState::Pressed
}
}
WindowEvent::Resized(size) => {
@@ -153,55 +203,81 @@ impl ApplicationHandler for App {
event_loop.exit();
}
WindowEvent::RedrawRequested => {
- // pixels logic
+ // compute frame delta
+ let now = Instant::now();
+ let secs_since_last_frame = (now - self.last_frame_real).as_secs_f32();
+ let delta_time = secs_since_last_frame / (1.0 / 60.0);
+ self.last_frame_real = now;
+ let instantaneous_fps = 1.0 / secs_since_last_frame;
+ // TODO: can smooth and round this
+ self.diagnostics.fps = instantaneous_fps;
+
+ // pixels/camera logic
let frame = pixels.frame_mut();
frame.fill(0);
- for y in 0..HEIGHT {
- for x in 0..WIDTH {
- let active = x % 2 == 0
- && y % 2 == 0
- && self.clicked.iter().all(|c| {
- (((x as i64) - (c.x as i64)).pow(2)
- + ((y as i64) - (c.y as i64)).pow(2))
- .isqrt() as f64
- > c.s
- });
+ // --TEST DRAWING--
+ if self.input.is_lmb_down {
+ // start with the bounding box of the drawing brush circle + some margin
+ let bb_xl = self.input.last_mouse_pos_on_board.0
+ - self.config.brush_size as i32 / 2
+ - 5;
+ let bb_xu = self.input.last_mouse_pos_on_board.0
+ + self.config.brush_size as i32 / 2
+ + 5;
+ let bb_yl = self.input.last_mouse_pos_on_board.1
+ - self.config.brush_size as i32 / 2
+ - 5;
+ let bb_yu = self.input.last_mouse_pos_on_board.1
+ + self.config.brush_size as i32 / 2
+ + 5;
- Self::set_px(
- frame,
- x,
- y,
- if active { self.red_level } else { 0x00 },
- if active { self.green_level } else { 0x00 },
- if active { self.blue_level } else { 0x00 },
- 0xFF,
- );
+ // for each point, check if the distance is less than the brush size and write the pixel
+ for x in bb_xl..bb_xu {
+ for y in bb_yl..bb_yu {
+ // brush/selection
+ if (((x - self.input.last_mouse_pos_on_board.0).pow(2)
+ + (y - self.input.last_mouse_pos_on_board.1).pow(2))
+ as f32)
+ .sqrt()
+ < self.config.brush_size as f32
+ {
+ board.set_cell_at_position(
+ x,
+ y,
+ sim::board::Cell {
+ material: 1,
+ velocity_x: 0,
+ velocity_y: 0,
+ flags: 0,
+ },
+ );
+ }
+ }
}
}
- for i in 0..self.clicked.len() {
- self.clicked[i].s -= 0.3;
- if self.clicked[i].s <= 0.0 {
- self.clicked.remove(i);
- break;
- }
- }
+ let get_overlay =
+ create_compute_combined_overlay_offset(board, &self.config, &self.input);
+ write_frame_view(frame, board, camera, get_overlay);
// egui logic
let raw_input = egui_state.take_egui_input(window);
let full_output = egui_context.run_ui(raw_input, |ui| {
- ui.heading("Debug");
- ui.label(
- self.clicked
- .iter()
- .map(|c| format!("x={x},y={y},s={s}", x = c.x, y = c.y, s = c.s))
- .collect::<Vec<String>>()
- .join("\n"),
- );
- ui.add(egui::Slider::new(&mut self.red_level, 0..=120).text("Red"));
- ui.add(egui::Slider::new(&mut self.green_level, 0..=120).text("Green"));
- ui.add(egui::Slider::new(&mut self.blue_level, 0..=120).text("Blue"));
+ let right_panel = egui::Panel::right(Id::new("right_panel"));
+ right_panel
+ .resizable(false)
+ // TODO collapse button
+ .show_collapsible(ui, &mut true, |panel_ui| {
+ draw_egui(
+ panel_ui,
+ &mut self.config,
+ &mut self.state,
+ camera,
+ &self.diagnostics,
+ &self.input,
+ )
+ });
});
egui_state.handle_platform_output(window, full_output.platform_output);
@@ -225,10 +301,8 @@ impl ApplicationHandler for App {
}
let _ = pixels.render_with(|encoder, render_target, context| {
- // 1. pixels' own blit of the framebuffer to the surface
context.scaling_renderer.render(encoder, render_target);
- // 2. prep egui's GPU-side buffers for this frame's draws
egui_renderer.update_buffers(
device,
&queue,
@@ -237,15 +311,14 @@ impl ApplicationHandler for App {
&screen_descriptor,
);
- // 3. open a pass on the same target, preserving the blit
- let mut pass = encoder
+ let mut egui_pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("egui pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: render_target,
resolve_target: None,
ops: wgpu::Operations {
- load: wgpu::LoadOp::Load, // keep the blit, don't clear
+ load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
@@ -255,10 +328,14 @@ impl ApplicationHandler for App {
occlusion_query_set: None,
multiview_mask: None,
})
- .forget_lifetime(); // bridge wgpu 30's pass-lifetime requirement
+ .forget_lifetime();
- egui_renderer.render(&mut pass, &clipped_primitives, &screen_descriptor);
- drop(pass);
+ egui_renderer.render(
+ &mut egui_pass,
+ &clipped_primitives,
+ &screen_descriptor,
+ );
+ drop(egui_pass);
Ok(())
});
@@ -270,9 +347,10 @@ impl ApplicationHandler for App {
fn about_to_wait(&mut self, _: &ActiveEventLoop) {
let now = Instant::now();
- // Limit to 60 FPS
- if now - self.last_frame >= FRAME_DURATION {
- self.last_frame = now;
+ let frame_duration: Duration = Duration::from_micros(1_000_000 / self.config.fps as u64);
+ // limit our internal redraw requests to (fps)
+ if now - self.last_frame_requested >= frame_duration {
+ self.last_frame_requested = now;
self.window
.expect("Bug - Window should exist")
.request_redraw();
diff --git a/src/sim/board.rs b/src/sim/board.rs
new file mode 100644
index 0000000..4cc5756
--- /dev/null
+++ b/src/sim/board.rs
@@ -0,0 +1,72 @@
+use crate::config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH};
+
+type MaterialId = u16;
+
+#[derive(Clone, Copy)]
+pub struct Cell {
+ pub material: MaterialId,
+ pub velocity_x: i8,
+ pub velocity_y: i8,
+ pub flags: u8,
+}
+
+impl Cell {
+ fn empty() -> Cell {
+ Cell {
+ material: 0,
+ velocity_x: 0,
+ velocity_y: 0,
+ flags: 0,
+ }
+ }
+}
+
+pub struct Board {
+ pub size_x: u32,
+ pub size_y: u32,
+ pub cells: Vec<Cell>,
+}
+
+impl Board {
+ pub fn index_to_position(&self, idx: usize) -> (i32, i32) {
+ let y = idx / self.size_x as usize;
+ let x = idx % self.size_x as usize;
+ let board_x = x as i32 - self.size_x as i32 / 2;
+ let board_y = y as i32 - self.size_x as i32 / 2;
+ return (board_x, board_y);
+ }
+ pub fn set_cell_at_position(&mut self, x: i32, y: i32, c: Cell) {
+ // TODO: option?
+ let idx = self.position_to_index(x, y).unwrap();
+ self.cells[idx] = c;
+ }
+ pub fn cell_at_position(&self, x: i32, y: i32) -> Option<&Cell> {
+ Some(&self.cells[self.position_to_index(x, y)?])
+ }
+ pub fn position_to_index(&self, x: i32, y: i32) -> Option<usize> {
+ let board_x = x + self.size_x as i32 / 2;
+ let board_y = y + self.size_y as i32 / 2;
+
+ let on_board = board_x >= 0
+ && board_x < self.size_x as i32
+ && board_y >= 0
+ && board_y < self.size_y as i32;
+
+ if !on_board {
+ return None;
+ }
+
+ return Some((board_y * self.size_x as i32 + board_x) as usize);
+ }
+ pub fn empty() -> Board {
+ let size_x = PIXEL_BUFFER_WIDTH * 2;
+ let size_y = PIXEL_BUFFER_HEIGHT * 2;
+ let cells: Vec<Cell> = vec![Cell::empty(); (size_x * size_y) as usize];
+
+ Board {
+ size_x,
+ size_y,
+ cells,
+ }
+ }
+}
diff --git a/src/sim/materials.rs b/src/sim/materials.rs
new file mode 100644
index 0000000..13a45f3
--- /dev/null
+++ b/src/sim/materials.rs
@@ -0,0 +1,22 @@
+#[derive(Clone, Copy)]
+pub struct Material<'a> {
+ pub name: &'a str,
+ pub r: u8,
+ pub g: u8,
+ pub b: u8,
+}
+
+pub static MATERIALS: [Material; 2] = [
+ Material {
+ name: "Void",
+ r: 0x00,
+ g: 0x00,
+ b: 0x00,
+ },
+ Material {
+ name: "Sand",
+ r: 0xFF,
+ g: 0x00,
+ b: 0xFF,
+ },
+];
diff --git a/src/sim/mod.rs b/src/sim/mod.rs
new file mode 100644
index 0000000..2b29628
--- /dev/null
+++ b/src/sim/mod.rs
@@ -0,0 +1,4 @@
+pub mod board;
+pub mod materials;
+pub mod overlay;
+pub mod sim;
diff --git a/src/sim/overlay.rs b/src/sim/overlay.rs
new file mode 100644
index 0000000..36a810b
--- /dev/null
+++ b/src/sim/overlay.rs
@@ -0,0 +1,50 @@
+use crate::{Config, Input, sim::board::Board};
+
+pub fn create_compute_combined_overlay_offset(
+ board: &Board,
+ config: &Config,
+ input: &Input,
+) -> impl Fn(i32, i32) -> (u8, u8, u8, u8) {
+ |x: i32, y: i32| {
+ // could allow negative offsets too
+ let mut offset: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0x00);
+
+ // bounds
+ // left
+ let xl = -((board.size_x / 2 + 1) as i32);
+ // right
+ let xu = (board.size_x / 2 + 1) as i32;
+ // bottom
+ let yl = -((board.size_y / 2 + 1) as i32);
+ // top
+ let yu = (board.size_y / 2 + 1) as i32;
+
+ if ((x == xl || x == xu) && (y <= yu && y >= yl))
+ || (y == yl || y == yu) && (x <= xu && x >= xl)
+ {
+ offset.0 = offset.0.saturating_add(0xFF);
+ offset.1 = offset.1.saturating_add(0xFF);
+ offset.2 = offset.2.saturating_add(0xFF);
+ }
+
+ // grid
+ if x % 30 == 0 || y % 30 == 0 {
+ offset.0 = offset.0.saturating_add(0x10);
+ offset.1 = offset.1.saturating_add(0x10);
+ offset.2 = offset.2.saturating_add(0x10);
+ }
+
+ // brush/selection
+ if (((x - input.last_mouse_pos_on_board.0).pow(2)
+ + (y - input.last_mouse_pos_on_board.1).pow(2)) as f32)
+ .sqrt()
+ < config.brush_size as f32
+ {
+ offset.0 = offset.0.saturating_add(0xAA);
+ offset.1 = offset.1.saturating_add(0x00);
+ offset.2 = offset.2.saturating_add(0xAA);
+ }
+
+ return offset;
+ }
+}
diff --git a/src/sim/sim.rs b/src/sim/sim.rs
new file mode 100644
index 0000000..bb68306
--- /dev/null
+++ b/src/sim/sim.rs
@@ -0,0 +1,3 @@
+use crate::Board;
+
+fn sim_tick(board: Board, seqno: u64) {}
diff --git a/src/ui.rs b/src/ui.rs
new file mode 100644
index 0000000..d55c5fb
--- /dev/null
+++ b/src/ui.rs
@@ -0,0 +1,33 @@
+use egui::Ui;
+
+use crate::{Config, Diagnostics, Input, State, camera::CameraState};
+
+pub fn draw_egui(
+ ui: &mut Ui,
+ config: &mut Config,
+ state: &mut State,
+ camera: &mut CameraState,
+ diagnostics: &Diagnostics,
+ input: &Input,
+) {
+ ui.heading("Config");
+ ui.add(egui::Slider::new(&mut config.brush_size, 1..=100).text("Brush size"));
+ ui.add(egui::Slider::new(&mut config.fps, 1..=1000).text("Max FPS"));
+ ui.label(format!("Real FPS: {}", diagnostics.fps));
+ ui.heading("Camera");
+ ui.add(egui::Slider::new(&mut camera.zoom, 0.0..=10.0).text("Zoom"));
+ ui.add(egui::Slider::new(&mut camera.x, -1000.0..=1000.0).text("X"));
+ ui.add(egui::Slider::new(&mut camera.y, -1000.0..=1000.0).text("Y"));
+ ui.heading("Input");
+ ui.label(format!(
+ "Mouse: x,y=({x}, {y}), lmb_pressed={lmb}",
+ x = input.last_mouse_pos_on_screen.0,
+ y = input.last_mouse_pos_on_screen.1,
+ lmb = input.is_lmb_down
+ ));
+ ui.label(format!(
+ "Mouse (board): x,y=({x}, {y})",
+ x = input.last_mouse_pos_on_board.0,
+ y = input.last_mouse_pos_on_board.1,
+ ));
+}