feat(toolbar): Parameters toolbar + click interaction

main
flavien 2022-12-24 19:44:30 +01:00
parent b9efb79fd9
commit 1af96098e3
8 changed files with 405 additions and 253 deletions

33
Cargo.lock generated
View File

@ -704,6 +704,12 @@ dependencies = [
"gl_generator",
]
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "ident_case"
version = "1.0.1"
@ -1193,6 +1199,12 @@ dependencies = [
"bitflags",
]
[[package]]
name = "rustversion"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
[[package]]
name = "safe_arch"
version = "0.5.2"
@ -1338,6 +1350,25 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "1.0.107"
@ -1644,6 +1675,8 @@ dependencies = [
"egui",
"getrandom",
"rand",
"strum",
"strum_macros",
"tracing-subscriber",
"tracing-wasm",
"wasm-bindgen-futures",

View File

@ -13,6 +13,12 @@ eframe = { version = "0.20.0", default-features = false, features = [
] }
rand = "0.8.5"
strum = "0.24.1"
strum_macros = "0.24.3"
[lib]
name = "gol"
path = "src/lib.rs"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@ -24,3 +30,7 @@ console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"
wasm-bindgen-futures = "0.4"
getrandom = { version = "0.2", features = ["js"] }
[profile.release]
opt-level = 's'
lto = true

View File

@ -29,15 +29,12 @@ You can now open the `index.html` file built in the `dist` folder in your browse
## TODO
### Game
toolbar UI with:
- pause/start/restart buttons
- speed/size change selectors
toolbar UI with:
- choose between known interesting seed or totally random one
- only live/dead colors mode or colored birth/death mode
interactions:
- click to kill or create a cell
interactions:
...
### Embedding
- embed in cyberduck.blog
- embed in cyberduck.blog

View File

@ -2,6 +2,6 @@
<head>
</head>
<body>
<canvas id="canvas" style="position: absolute; top: 0%; left: 0%; width: 1600px; height: 900px;"></canvas>
<canvas id="canvas" style="position: absolute; top: 0%; left: 0%; width: 1280px; height: 720px;"></canvas>
</body>
</html>

201
src/app.rs Normal file
View File

@ -0,0 +1,201 @@
use std::fmt::{Display, Formatter, Result};
use eframe::App;
use egui::{
vec2, CentralPanel, ComboBox, Context, Frame, Pos2, Rect, Sense, Shape, TextStyle,
TopBottomPanel,
};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use crate::game::Universe;
const CANVAS_WIDTH: f32 = 1280.0;
const CANVAS_HEIGHT: f32 = 720.0;
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
enum Speed {
Slow = 8,
Normal = 4,
Fast = 2,
Fastest = 1,
}
impl Default for Speed {
fn default() -> Self {
Speed::Fast
}
}
impl Display for Speed {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Speed::Slow => write!(f, "Slow"),
Speed::Normal => write!(f, "Normal"),
Speed::Fast => write!(f, "Fast"),
Speed::Fastest => write!(f, "Fastest"),
}
}
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
enum Size {
Small = 8,
Medium = 4,
Large = 2,
Giant = 1,
}
impl Default for Size {
fn default() -> Self {
Size::Large
}
}
impl Display for Size {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Size::Small => write!(f, "Small"),
Size::Medium => write!(f, "Medium"),
Size::Large => write!(f, "Large"),
Size::Giant => write!(f, "Giant"),
}
}
}
pub struct MyEguiApp {
game: Universe,
frame_counter: u8,
speed: Speed,
size: Size,
cache: Vec<Shape>,
paused: bool,
click: Option<Pos2>,
}
impl MyEguiApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
let size = Size::default();
Self {
game: Universe::new(
CANVAS_WIDTH as u32 / size as u32,
CANVAS_HEIGHT as u32 / size as u32,
),
frame_counter: 0,
speed: Speed::default(),
size,
cache: Vec::new(),
paused: false,
click: None,
}
}
}
impl App for MyEguiApp {
fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) {
let toolbar = self.render_toolbar(ctx);
#[cfg(not(target_arch = "wasm32"))]
frame.set_window_size(vec2(CANVAS_WIDTH, CANVAS_HEIGHT + toolbar.height()));
self.render_canvas(ctx);
if !self.paused {
self.frame_counter += 1;
self.frame_counter %= self.speed as u8;
}
ctx.request_repaint();
}
}
impl MyEguiApp {
fn reset(&mut self) {
self.game = Universe::new(
CANVAS_WIDTH as u32 / self.size as u32,
CANVAS_HEIGHT as u32 / self.size as u32,
);
self.cache = Vec::new();
self.frame_counter = 0;
}
fn render_toolbar(&mut self, ctx: &Context) -> Rect {
let mut frame = egui::Frame::side_top_panel(ctx.style().as_ref());
frame.inner_margin.bottom += 1.5;
let toolbar = TopBottomPanel::top("my_panel")
.frame(frame)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.style_mut().spacing.interact_size.y *= 1.25;
ui.style_mut()
.text_styles
.get_mut(&TextStyle::Button)
.unwrap()
.size *= 1.25;
if ui.button("Reset").clicked() {
self.reset();
}
let paused = self.paused;
ui.toggle_value(&mut self.paused, if paused { "" } else { "" });
ComboBox::from_label("Speed")
.selected_text(format!("{:?}", self.speed))
.show_ui(ui, |ui| {
for speed in Speed::iter() {
if ui
.selectable_value(&mut self.speed, speed, speed.to_string())
.clicked()
{
self.frame_counter = 0;
}
}
});
ComboBox::from_label("Size")
.selected_text(format!("{:?}", self.size))
.show_ui(ui, |ui| {
for size in Size::iter() {
if ui
.selectable_value(&mut self.size, size, size.to_string())
.clicked()
{
self.reset();
}
}
});
});
});
toolbar.response.rect
}
fn render_canvas(&mut self, ctx: &Context) -> Rect {
let frame = Frame::central_panel(ctx.style().as_ref()).inner_margin(0.0);
let canvas = CentralPanel::default().frame(frame).show(ctx, |ui| {
let (res, painter) =
ui.allocate_painter(vec2(CANVAS_WIDTH, CANVAS_HEIGHT), Sense::click());
let canvas_origin = res.rect.left_top().to_vec2();
let interaction = match (res.interact_pointer_pos(), self.click) {
(Some(pointer_pos), None) => {
let canvas_pos = pointer_pos - canvas_origin;
self.click = Some(canvas_pos);
let x = canvas_pos.x as u32 / self.size as u32;
let y = canvas_pos.y as u32 / self.size as u32;
self.game.toggle_cell(y, x);
true
}
(None, Some(_)) => {
self.click = None;
false
}
_ => false,
};
let tick = !self.paused && self.frame_counter % self.speed as u8 == 0;
if tick {
self.game.tick();
}
if tick || interaction || self.cache.is_empty() {
self.cache = self
.game
.render(canvas_origin, vec2(CANVAS_WIDTH, CANVAS_HEIGHT));
}
painter.extend(self.cache.iter().cloned());
});
canvas.response.rect
}
}

153
src/game.rs Normal file
View File

@ -0,0 +1,153 @@
use eframe::epaint::RectShape;
use egui::{pos2, vec2, Color32, Rect, Rounding, Shape, Stroke, Vec2};
use rand::Rng;
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
pub struct Universe {
width: u32,
height: u32,
age: u32,
cells: (Vec<Cell>, Vec<Cell>),
}
impl Universe {
pub fn new(width: u32, height: u32) -> Universe {
let mut rng = rand::thread_rng();
let cells = (0..width * height)
.map(|_| {
if rng.gen_range(0..10) > 5 {
Cell::Alive
} else {
Cell::Dead
}
})
.collect::<Vec<Cell>>();
Universe {
width,
height,
age: 0,
cells: (cells.clone(), cells),
}
}
pub fn render(&self, canvas_origin: Vec2, canvas_size: Vec2) -> Vec<Shape> {
let fill = Color32::from_gray(128);
let rounding = Rounding::none();
let cell_side = canvas_size.x / self.width as f32;
let stroke = Stroke::NONE;
let mut rectangles_cache = Vec::new();
let current = match self.age % 2 {
0 => &self.cells.1,
_ => &self.cells.0,
};
let mut width_counter = 0;
let mut width_origin = 0.0;
for row in 0..self.height {
for col in 0..self.width {
let idx = Universe::get_index(self.width, row, col);
if let Cell::Alive = current[idx] {
if width_counter == 0 {
width_origin = col as f32 * cell_side;
}
width_counter += 1;
} else if width_counter != 0 {
let y = row as f32 * cell_side;
let rect = Rect::from_min_size(
pos2(width_origin, y) + canvas_origin,
vec2(cell_side * width_counter as f32, cell_side),
);
rectangles_cache.push(Shape::Rect(RectShape {
rect,
rounding,
fill,
stroke,
}));
width_counter = 0;
}
}
}
rectangles_cache
}
fn get_index(width: u32, row: u32, column: u32) -> usize {
(row * width + column) as usize
}
fn live_neighbor_count(
universe: &[Cell],
height: u32,
width: u32,
row: u32,
column: u32,
) -> u8 {
let mut count = 0;
for delta_row in [height - 1, 0, 1] {
for delta_col in [width - 1, 0, 1] {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % height;
let neighbor_col = (column + delta_col) % width;
let idx = Universe::get_index(width, neighbor_row, neighbor_col);
count += universe[idx] as u8;
}
}
count
}
pub fn tick(&mut self) {
let (next, current) = match self.age % 2 {
0 => (&mut self.cells.0, &self.cells.1),
_ => (&mut self.cells.1, &self.cells.0),
};
for row in 0..self.height {
for col in 0..self.width {
let idx = Universe::get_index(self.width, row, col);
let cell = current[idx];
let live_neighbors =
Universe::live_neighbor_count(current, self.height, self.width, row, col);
let next_cell = match (cell, live_neighbors) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.age += 1;
}
pub fn toggle_cell(&mut self, row: u32, column: u32) {
let idx = Universe::get_index(self.width, row, column);
let current = match self.age % 2 {
0 => &mut self.cells.1,
_ => &mut self.cells.0,
};
current[idx] = match current[idx] {
Cell::Dead => Cell::Alive,
Cell::Alive => Cell::Dead,
};
}
}

2
src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod app;
pub mod game;

View File

@ -1,169 +1,4 @@
use eframe::epaint::RectShape;
use egui::{pos2, Color32, Rect, Rounding, Sense, Shape, Stroke, Vec2};
use rand::Rng;
use std::fmt;
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
pub struct Universe {
width: u32,
height: u32,
age: u32,
cells: (Vec<Cell>, Vec<Cell>),
}
impl Universe {
pub fn new(width: u32, height: u32) -> Universe {
let mut rng = rand::thread_rng();
let cells = (0..width * height)
.map(|_| {
if rng.gen_range(0..10) > 5 {
Cell::Alive
} else {
Cell::Dead
}
})
.collect::<Vec<Cell>>();
Universe {
width,
height,
age: 0,
cells: (cells.clone(), cells),
}
}
// pub fn render(&self) -> String {
// self.to_string()
// }
pub fn render(&self, canvas_origin: Vec2, canvas_size: Vec2) -> Vec<Shape> {
let fill = Color32::from_gray(128);
let rounding = Rounding::none();
let cell_side = canvas_size.x / self.width as f32;
let stroke = Stroke::NONE;
let mut rectangles_cache = Vec::new();
let current = match self.age % 2 {
0 => &self.cells.0,
_ => &self.cells.1,
};
let mut width_counter = 0;
let mut width_origin = 0.0;
for row in 0..self.height {
for col in 0..self.width {
let idx = Universe::get_index(self.width, row, col);
if let Cell::Alive = current[idx] {
if width_counter == 0 {
width_origin = col as f32 * cell_side;
}
width_counter += 1;
} else if width_counter != 0 {
let y = row as f32 * cell_side;
let rect = Rect::from_min_size(
pos2(width_origin, y) + canvas_origin,
Vec2::new(cell_side * width_counter as f32, cell_side),
);
rectangles_cache.push(Shape::Rect(RectShape {
rect,
rounding,
fill,
stroke,
}));
width_counter = 0;
}
}
}
rectangles_cache
}
fn get_index(width: u32, row: u32, column: u32) -> usize {
(row * width + column) as usize
}
fn live_neighbor_count(
universe: &[Cell],
height: u32,
width: u32,
row: u32,
column: u32,
) -> u8 {
let mut count = 0;
for delta_row in [height - 1, 0, 1] {
for delta_col in [width - 1, 0, 1] {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % height;
let neighbor_col = (column + delta_col) % width;
let idx = Universe::get_index(width, neighbor_row, neighbor_col);
count += universe[idx] as u8;
}
}
count
}
pub fn tick(&mut self) {
let (next, current) = match self.age % 2 {
0 => (&mut self.cells.0, &self.cells.1),
_ => (&mut self.cells.1, &self.cells.0),
};
for row in 0..self.height {
for col in 0..self.width {
let idx = Universe::get_index(self.width, row, col);
let cell = current[idx];
let live_neighbors =
Universe::live_neighbor_count(current, self.height, self.width, row, col);
let next_cell = match (cell, live_neighbors) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.age += 1;
}
}
impl fmt::Display for Universe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let current = match self.age % 2 {
0 => &self.cells.0,
_ => &self.cells.1,
};
for line in current.as_slice().chunks(self.width as usize) {
for &cell in line {
let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
write!(f, "{}", symbol)?;
}
writeln!(f)?;
}
Ok(())
}
}
const CANVAS_WIDTH: f32 = 1600.0;
const CANVAS_HEIGHT: f32 = 900.0;
use gol::app::MyEguiApp;
#[cfg(target_arch = "wasm32")]
fn main() {
@ -188,92 +23,13 @@ fn main() {
#[cfg(not(target_arch = "wasm32"))]
fn main() {
use egui::vec2;
let options = eframe::NativeOptions {
initial_window_size: Some(vec2(CANVAS_WIDTH, CANVAS_HEIGHT)),
resizable: false,
..Default::default()
};
eframe::run_native(
"My egui App",
"Game of Life",
options,
Box::new(|cc| Box::new(MyEguiApp::new(cc))),
);
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Speed {
//Slow = 16,
//Normal = 8,
//Fast = 4,
Fastest = 2,
}
impl Default for Speed {
fn default() -> Self {
Speed::Fastest
}
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Size {
//Small = 8,
//Medium = 4,
Large = 2,
}
impl Default for Size {
fn default() -> Self {
Size::Large
}
}
struct MyEguiApp {
game: Universe,
frame_counter: u8,
speed: Speed,
//size: Size,
cache: Vec<Shape>,
}
impl MyEguiApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
// Restore app state using cc.storage (requires the "persistence" feature).
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
// for e.g. egui::PaintCallback.
let size = Size::default();
Self {
game: Universe::new(
CANVAS_WIDTH as u32 / size as u32,
CANVAS_HEIGHT as u32 / size as u32,
),
frame_counter: 0,
speed: Speed::default(),
//size,
cache: Vec::new(),
}
}
}
impl eframe::App for MyEguiApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
const PAINTER_SIZE: Vec2 = egui::vec2(CANVAS_WIDTH, CANVAS_HEIGHT);
egui::CentralPanel::default().show(ctx, |ui| {
// add click sense later for interaction
let (response, painter) = ui.allocate_painter(PAINTER_SIZE, Sense::hover());
let origin = response.rect.left_top().to_vec2();
if self.frame_counter % self.speed as u8 == 0 {
self.game.tick();
self.cache = self.game.render(origin, PAINTER_SIZE);
}
painter.extend(self.cache.iter().cloned());
});
self.frame_counter += 1;
self.frame_counter %= self.speed as u8;
ctx.request_repaint();
}
}