Skip to content
Snippets Groups Projects
Commit 319a29b5 authored by Finn Bear's avatar Finn Bear
Browse files

Arrows.

parent 640a9e41
No related branches found
No related tags found
No related merge requests found
......@@ -868,6 +868,7 @@ dependencies = [
"egui_demo_lib",
"image",
"rand",
"rand_chacha",
]
[[package]]
......
......@@ -7,4 +7,5 @@ edition = "2021"
eframe = "0.17"
egui_demo_lib = {git = "https://github.com/finnbear/egui", branch = "code_theme_font_size_3"}
image = {version = "0.24", default-features = false, features=["png"]}
rand = {version = "0.8"}
\ No newline at end of file
rand = "0.8"
rand_chacha = "0.3"
\ No newline at end of file
......@@ -70,7 +70,7 @@ impl epi::App for Cartoon {
// Light theme.
style.visuals = Visuals::light();
// Larger margin for windows (which are used for more control when positing stuff).
style.spacing.window_margin = Margin::same(24.0);
style.spacing.window_margin = Margin::same(16.0);
// Increase font sizes a lot.
{
......
pub mod arrow;
/// See component directory. This file exists to make the individual components accessible to the
/// rest of the code.
pub mod code;
......
use crate::egui::{Pos2, Stroke, Ui};
use eframe::egui::Shape;
use eframe::emath::Rot2;
/// Pointy line used to emphasize something.
pub struct Arrow {
pub origin: Pos2,
pub tip: Pos2,
pub stroke: Stroke,
}
impl Arrow {
pub fn show(&self, ui: &mut Ui) {
// Based on: https://docs.rs/egui/latest/src/egui/widgets/plot/items/mod.rs.html#977-985
let vector = self.tip - self.origin;
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
let tip_length = self.stroke.width * 4.0;
let tip = self.origin + vector;
let dir = vector.normalized();
ui.painter()
.add(Shape::line_segment([self.origin, self.tip], self.stroke));
ui.painter().add(Shape::line(
vec![
tip - tip_length * (rot.inverse() * dir),
tip,
tip - tip_length * (rot * dir),
],
self.stroke,
));
}
}
use crate::color::{set_alpha, set_style_alpha};
use crate::egui::Frame;
use eframe::egui::{Align2, Color32, Context, Pos2, Rect, Rounding, Shape, Stroke, Vec2, Window};
use eframe::emath;
use eframe::egui::{
Align2, Color32, Context, Pos2, Rect, Rounding, Shape, Stroke, Ui, Vec2, Window,
};
use eframe::emath::RectTransform;
use eframe::epaint::RectShape;
/// A reusable grid component.
......@@ -103,8 +105,22 @@ impl Grid {
.map(|(i, c)| (i % self.size_cells, i / self.size_cells, c))
}
/// Render to UI.
pub fn center(&self, x: usize, y: usize) -> Pos2 {
let x_coord = (x as f32 + 0.5) / self.size_cells as f32;
let y_coord = (y as f32 + 0.5) / self.size_cells as f32;
Pos2::new(x_coord, y_coord)
}
pub fn show(&self, ctx: &Context) {
self.show_with_overlays(ctx, |_, _| {})
}
/// Render to UI.
pub fn show_with_overlays<R>(
&self,
ctx: &Context,
add_overlays: impl FnOnce(&mut Ui, &RectTransform) -> R,
) {
Window::new(self.name)
.title_bar(false)
.frame(Frame::window(&set_style_alpha(&ctx.style(), self.alpha)))
......@@ -115,10 +131,8 @@ impl Grid {
.fixed_size(Vec2::splat(self.size_pixels))
.show(ctx, |ui| {
let (_id, rect) = ui.allocate_space(Vec2::splat(self.size_pixels));
let to_screen = emath::RectTransform::from_to(
Rect::from_x_y_ranges(0.0..=1.0, 0.0..=1.0),
rect,
);
let to_screen =
RectTransform::from_to(Rect::from_x_y_ranges(0.0..=1.0, 0.0..=1.0), rect);
let background = set_alpha(self.background, self.alpha);
if background.a() > 0 {
......@@ -175,6 +189,8 @@ impl Grid {
});
}
}
add_overlays(ui, &to_screen);
});
}
}
......@@ -17,3 +17,18 @@ pub fn fade_in<R>(
}
with_alpha(ui, alpha, |ui| add_contents(ui))
}
/// Like `fade_in` but provides alpha value for manual fading.
pub fn fade_in_manual<R>(
ui: &mut Ui,
fade_start: f64,
add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> R {
let since_transition = ui.ctx().input().time - fade_start;
let alpha = (since_transition * (1.0 / FADE_DURATION)).min(1.0) as f32;
if alpha < 1.0 {
// The fade isn't done yet, so schedule another render.
ui.ctx().request_repaint();
}
add_contents(ui, alpha)
}
use crate::color::set_alpha;
use crate::component::arrow::Arrow;
use crate::component::grid::Grid;
use crate::egui::{Align2, Context, FontFamily, FontId, Stroke};
use crate::fade_in::fade_in_manual;
use crate::governor::Governor;
use crate::slide::Slide;
use eframe::egui::style::Margin;
use eframe::egui::{Color32, Frame, Ui};
use rand::Rng;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
/// Conway's Game of Life, etc.
pub struct Automata {
/// Persistent state of the grid.
life: Grid,
/// State of the transitions.
state: AutomataState,
/// For knowing when to advance the simulation.
governor: Governor,
}
#[derive(Default)]
enum AutomataState {
/// Nothing on the grid.
#[default]
Empty,
/// Random colors on the grid, but no simulation.
Static {
/// When started fading in "alive" arrow.
alive: Option<f64>,
/// When started fading in "alive" arrow.
dead: Option<f64>,
},
/// Simulation.
Life,
}
impl Default for Automata {
fn default() -> Self {
let mut life = Grid::default();
......@@ -20,6 +43,7 @@ impl Default for Automata {
life.name = "life";
// Fill grid with random cells.
/*
for y in 0..life.size_cells {
for x in 0..life.size_cells {
if rand::thread_rng().gen_bool(0.5) {
......@@ -27,6 +51,7 @@ impl Default for Automata {
}
}
}
*/
// This would replace the grid with a single oscillator.
/*
......@@ -38,12 +63,47 @@ impl Default for Automata {
Self {
life,
state: AutomataState::default(),
governor: Governor::default(),
}
}
}
impl Slide for Automata {
fn transition(&mut self, ctx: &Context) -> bool {
match &mut self.state {
AutomataState::Empty => {
self.state = AutomataState::Static {
alive: None,
dead: None,
};
let mut rng = ChaCha8Rng::seed_from_u64(123456789876543216);
// Randomize grid.
for y in 0..self.life.size_cells {
for x in 0..self.life.size_cells {
if rng.gen_bool(0.3) {
self.life.set_fill(x, y, Color32::BLACK);
}
}
}
}
AutomataState::Static { alive, dead } => {
// Enable arrows one by one.
if alive.is_none() {
*alive = Some(ctx.input().time);
} else if dead.is_none() {
*dead = Some(ctx.input().time);
} else {
self.state = AutomataState::Life;
}
}
AutomataState::Life => return true,
}
false
}
fn show(&mut self, ui: &mut Ui) {
Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
ui.vertical_centered(|ui| {
......@@ -54,12 +114,57 @@ impl Slide for Automata {
// Need to continuously animate the grid, or at least poll the governor.
ui.ctx().request_repaint();
// Iterate grid ~5 times a second.
if self.governor.ready(ui.ctx().input().time, 0.2) {
self.life = conways_game_of_life(&self.life);
match &self.state {
AutomataState::Empty => {}
AutomataState::Static { .. } => {}
AutomataState::Life => {
// Iterate grid ~5 times a second.
if self.governor.ready(ui.ctx().input().time, 0.2) {
self.life = conways_game_of_life(&self.life);
}
}
}
self.life.show(ui.ctx());
self.life.show_with_overlays(ui.ctx(), |ui, to_screen| {
if let &AutomataState::Static { alive, dead } = &self.state {
if let Some(alive) = alive {
fade_in_manual(ui, alive, |ui, alpha| {
let color = set_alpha(Color32::GREEN, alpha);
Arrow {
origin: to_screen * self.life.center(4, 2),
tip: to_screen * self.life.center(7, 7),
stroke: Stroke::new(6.0, color),
}
.show(ui);
ui.painter().text(
to_screen * self.life.center(4, 2),
Align2::RIGHT_BOTTOM,
"Alive",
FontId::new(40.0, FontFamily::Proportional),
color,
);
});
}
if let Some(dead) = dead {
fade_in_manual(ui, dead, |ui, alpha| {
let color = set_alpha(Color32::DARK_GREEN, alpha);
Arrow {
origin: to_screen * self.life.center(11, 13),
tip: to_screen * self.life.center(10, 10),
stroke: Stroke::new(6.0, color),
}
.show(ui);
ui.painter().text(
to_screen * self.life.center(11, 13),
Align2::LEFT_TOP,
"Dead",
FontId::new(40.0, FontFamily::Proportional),
color,
);
});
}
}
});
}
}
......
......@@ -5,6 +5,7 @@ mod rectangle;
use crate::component::code::{pseudocode, Code};
use crate::component::grid::Grid;
use crate::egui::{Color32, Context, Frame, Ui};
use crate::governor::Governor;
use crate::slide::s5_fractals::circle::circle;
use crate::slide::s5_fractals::mandelbrot::mandelbrot;
use crate::slide::s5_fractals::rectangle::rectangle;
......@@ -15,6 +16,8 @@ use eframe::egui::style::Margin;
pub struct Fractals {
/// Where we are in the transitions.
state: FractalsState,
/// For showing more pixels.
governor: Governor,
/// Pseudocode display.
code: Code,
/// Persistent grid state.
......@@ -33,16 +36,16 @@ enum FractalsState {
fade_start: f64,
},
Rectangle {
/// TODO(finnb): How many pixels of the grid we've filled in.
pixel: usize,
/// How many pixels of the grid we've filled in.
pixels: usize,
},
Circle {
/// TODO(finnb): How many pixels of the grid we've filled in.
pixel: usize,
/// How many pixels of the grid we've filled in.
pixels: usize,
},
Mandelbrot {
/// TODO(finnb): How many pixels of the grid we've filled in.
pixel: usize,
/// How many pixels of the grid we've filled in.
pixels: usize,
},
}
......@@ -60,13 +63,14 @@ impl Default for Fractals {
grid.name = "fractal_grid";
grid.offset_x = HORIZONTAL_OFFSET;
grid.size_cells = 256;
grid.size_cells = 128;
// Grid cell stroke aliases too much at this resolution.
grid.stroke_width = 0.0;
Self {
state: FractalsState::default(),
governor: Governor::default(),
code,
grid,
}
......@@ -87,10 +91,10 @@ impl Slide for Fractals {
self.code.alpha = 1.0;
self.grid.alpha = 1.0;
self.state = FractalsState::Rectangle { pixel: 0 }
self.state = FractalsState::Rectangle { pixels: 0 }
}
FractalsState::Rectangle { .. } => self.state = FractalsState::Circle { pixel: 0 },
FractalsState::Circle { .. } => self.state = FractalsState::Mandelbrot { pixel: 0 },
FractalsState::Rectangle { .. } => self.state = FractalsState::Circle { pixels: 0 },
FractalsState::Circle { .. } => self.state = FractalsState::Mandelbrot { pixels: 0 },
FractalsState::Mandelbrot { .. } => return true,
}
false
......@@ -113,12 +117,16 @@ impl Slide for Fractals {
// away with that since the function doesn't leave our stack frame.
let algo: &dyn Fn(f32, f32) -> bool;
match self.state {
// Limit to this many pixels. If [`None`], will render all pixels.
// We use a mutable reference so we can increment in one place.
let mut limit_pixels: Option<&mut usize> = None;
match &mut self.state {
FractalsState::Before => {
// Don't proceed to render the code/grid.
return;
}
FractalsState::Grid { fade_start } => {
&mut FractalsState::Grid { fade_start } => {
// Fade in the code and grid.
let elapsed = ui.ctx().input().time - fade_start;
let alpha = (elapsed * 2.0).min(1.0) as f32;
......@@ -126,29 +134,48 @@ impl Slide for Fractals {
self.grid.alpha = alpha;
algo = &|_, _| false;
}
FractalsState::Rectangle { .. } => {
FractalsState::Rectangle { pixels } => {
self.code.code = pseudocode(include_str!("s5_fractals/rectangle.rs"));
algo = &rectangle;
limit_pixels = Some(pixels)
}
FractalsState::Circle { .. } => {
FractalsState::Circle { pixels } => {
self.code.code = pseudocode(include_str!("s5_fractals/circle.rs"));
algo = &circle;
limit_pixels = Some(pixels);
}
FractalsState::Mandelbrot { .. } => {
FractalsState::Mandelbrot { pixels } => {
self.code.code = pseudocode(include_str!("s5_fractals/mandelbrot.rs"));
algo = &mandelbrot;
limit_pixels = Some(pixels);
}
}
// Double mutable borrow to avoid moving limit_pixels.
if let Some(limit_pixels) = &mut limit_pixels {
if **limit_pixels < self.grid.size_cells.pow(2)
&& self.governor.ready(ui.ctx().input().time, 0.02)
{
**limit_pixels += self.grid.size_cells.pow(2) * 33 / 32 / 32;
}
}
// TODO: Different demos may need different resolutions.
const RESOLUTION: usize = 256;
// Effective limit for pixels.
let effective_limit = limit_pixels.map(|l| *l).unwrap_or(usize::MAX);
// Store the resolution before mutably borrowing self.grid.
let size_cells = self.grid.size_cells;
// Paint the grid.
// TODO(finnb): Paint pixel by pixel.
for (gx, gy, c) in self.grid.iter_mut() {
for (i, (gx, gy, c)) in self.grid.iter_mut().enumerate() {
if i >= effective_limit {
*c = Color32::TRANSPARENT;
continue;
}
// 0 to 1.
let nx = (gx as f32 + 0.5) / RESOLUTION as f32;
let ny = (gy as f32 + 0.5) / RESOLUTION as f32;
let nx = (gx as f32 + 0.5) / size_cells as f32;
let ny = (gy as f32 + 0.5) / size_cells as f32;
// -4 to 4.
let x = nx * 4.0 - 2.0;
......@@ -157,7 +184,7 @@ impl Slide for Fractals {
*c = if algo(x, y) {
Color32::BLACK
} else {
Color32::TRANSPARENT
Color32::WHITE
};
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment