diff --git a/src/component.rs b/src/component.rs index 1140c84a90cdd26a06cb6e7d7dcccacd55c4aadf..c72f31f7cfb019ae37f5ffe13651b1ee6ed4fbec 100644 --- a/src/component.rs +++ b/src/component.rs @@ -4,4 +4,5 @@ pub mod arrow; pub mod code; pub mod grid; +pub mod image; pub mod triangle; diff --git a/src/component/image.rs b/src/component/image.rs new file mode 100644 index 0000000000000000000000000000000000000000..af14a04bc9d2fb280baed0a91c7ebd22b4e446a0 --- /dev/null +++ b/src/component/image.rs @@ -0,0 +1,71 @@ +use crate::color::{set_alpha, set_style_alpha}; +use crate::egui::{Color32, TextureHandle, Ui}; +use eframe::egui; +use eframe::egui::style::Margin; +use eframe::egui::{Frame, Pos2, Vec2, Widget, Window}; + +/// A bitmap image i.e. an example of generative art. +pub struct Image { + /// How many pixels of height the images should occupy. + pub height: f32, + /// How opaque the image is. + pub alpha: f32, + /// Center position. + pub position: Option<Pos2>, +} + +impl Default for Image { + fn default() -> Self { + Self::new(128.0, 1.0, None) + } +} + +impl Image { + pub fn new(height: f32, alpha: f32, position: Option<Pos2>) -> Self { + Self { + height, + alpha, + position, + } + } + + /// Change the height of the image. + pub fn height(mut self, height: f32) -> Self { + self.height = height; + self + } + + /// Change the alpha of the image. + pub fn alpha(mut self, alpha: f32) -> Self { + self.alpha = alpha; + self + } + + /// Change the position of the image. + pub fn position(mut self, position: Pos2) -> Self { + // We assume that, if you call this method, you don't want [`None`]. + self.position = Some(position); + self + } + + pub fn show(&mut self, ui: &mut Ui, texture: &TextureHandle) { + let mut window = Window::new(texture.name()) + .title_bar(false) + .resizable(false) + // Reduce margin of images. + .frame( + Frame::window(&set_style_alpha(ui.style(), self.alpha)).margin(Margin::same(5.0)), + ); + + if let Some(position) = self.position { + window = window.default_pos(position).fixed_pos(position); + } + + window.show(ui.ctx(), |ui| { + // TODO: Support non-square images. + egui::Image::new(texture, Vec2::splat(self.height)) + .tint(set_alpha(Color32::WHITE, self.alpha)) + .ui(ui); + }); + } +} diff --git a/src/fade_in.rs b/src/fade_in.rs index 709ed89228d724937d66911a5f5656b4da121a3f..6778c6bf6a0354f8595d99a7b4d9a22181b53913 100644 --- a/src/fade_in.rs +++ b/src/fade_in.rs @@ -10,7 +10,7 @@ pub fn fade_in<R>( add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse<R> { let since_transition = ui.ctx().input().time - fade_start; - let alpha = (since_transition * (1.0 / FADE_DURATION)).min(1.0) as f32; + let alpha = (since_transition * (1.0 / FADE_DURATION)).clamp(0.0, 1.0) as f32; if alpha < 1.0 { // The fade isn't done yet, so schedule another render. ui.ctx().request_repaint(); diff --git a/src/image/fluid0.png b/src/image/fluid0.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee8ff33575215fc0333c3ebe47d19235a84e2fc Binary files /dev/null and b/src/image/fluid0.png differ diff --git a/src/image/raytracing0.png b/src/image/raytracing0.png new file mode 100644 index 0000000000000000000000000000000000000000..b3d95a395260668a3dadb117463ba56d6571fd7b Binary files /dev/null and b/src/image/raytracing0.png differ diff --git a/src/slide/s1_title.rs b/src/slide/s1_title.rs index 9e4c09715eb32fee5774ff58069c263459d233d7..17639e20d514782a8bedf535a7c2fe8d6f74ebee 100644 --- a/src/slide/s1_title.rs +++ b/src/slide/s1_title.rs @@ -1,3 +1,4 @@ +use crate::component::image::Image; use crate::egui::{Align2, Context}; use crate::fade_in::fade_in; use crate::slide::Slide; @@ -47,14 +48,7 @@ impl Slide for Title { } for example in &self.examples { - Window::new(example.name()) - .title_bar(false) - .resizable(false) - // Reduce margin of example images. - .frame(Frame::window(&ui.style()).margin(Margin::same(5.0))) - .show(ui.ctx(), |ui| { - ui.image(example, Vec2::splat(128.0)); - }); + Image::default().show(ui, example); } if let &TitleState::Title { fade_start } = &self.state { diff --git a/src/slide/s4_automata.rs b/src/slide/s4_automata.rs index 292855c7fd5df390356299d988ec66468ef516fb..e0551c36a4110b0dedc1ac4a2cdf494b053659b2 100644 --- a/src/slide/s4_automata.rs +++ b/src/slide/s4_automata.rs @@ -1,6 +1,7 @@ use crate::color::set_alpha; use crate::component::arrow::Arrow; use crate::component::grid::Grid; +use crate::component::image::Image; use crate::ctx_img; use crate::egui::{Align2, Context, TextureHandle, Vec2, Window}; use crate::fade_in::{fade_in, fade_in_manual}; @@ -119,17 +120,11 @@ impl Slide for Automata { }); // TODO: Fade/move/position as appropriate. - Window::new("portrait") - .title_bar(false) - .resizable(false) - .frame(Frame::window(&ui.style()).margin(Margin::same(5.0))) - .show(ui.ctx(), |ui| { - ui.image( - self.conway_portrait - .get_or_insert_with(|| ctx_img!(ui.ctx(), "conway_portrait.png")), - Vec2::splat(128.0), - ); - }); + Image::default().show( + ui, + self.conway_portrait + .get_or_insert_with(|| ctx_img!(ui.ctx(), "conway_portrait.png")), + ); // Need to continuously animate the grid, or at least poll the governor. ui.ctx().request_repaint(); @@ -156,7 +151,7 @@ impl Slide for Automata { } => { // Iterate grid ~5 times a second. if self.governor.ready(ui.ctx().input().time, 0.2) { - // Schedule some special events at some interation counts. + // Schedule some special events at some iteration counts. match *iterations { 10 => { // Automatically begin expansion. @@ -201,7 +196,9 @@ impl Slide for Automata { } if let Some(rules_fade) = rules_fade { - fade_in(ui, rules_fade, |ui| { + // Give some time for the grid to slide left. + const RULES_DELAY: f64 = 0.3; + fade_in(ui, rules_fade + RULES_DELAY, |ui| { Window::new("conway_rules") .frame(Frame::window(&ui.style())) .anchor(Align2::CENTER_CENTER, Vec2::new(HORIZONTAL_OFFSET, 0.0)) @@ -209,7 +206,7 @@ impl Slide for Automata { .resizable(false) .show(ui.ctx(), |ui| { // Nested fade in since fade doesn't propagate from [`Window`] to children. - fade_in(ui, rules_fade, |ui| { + fade_in(ui, rules_fade + RULES_DELAY, |ui| { ui.label(" âµ Cells with fewer than two neighbors die"); ui.label(" âµ Cells with more than three neighbors die"); ui.label(" âµ Cells with three neighbors become alive"); diff --git a/src/slide/s6_computation.rs b/src/slide/s6_computation.rs index 660de167366910b29afa928c0b52c9261e964041..02c78bb007e4d967b985adb871717c16f2a5910d 100644 --- a/src/slide/s6_computation.rs +++ b/src/slide/s6_computation.rs @@ -1,19 +1,87 @@ -use crate::egui::{Frame, Ui}; +use crate::component::image::Image; +use crate::ctx_img; +use crate::egui::{Context, Frame, Ui}; +use crate::fade_in::{fade_in, fade_in_manual}; use crate::slide::Slide; use eframe::egui::style::Margin; +use eframe::egui::{Pos2, TextureHandle}; #[derive(Default)] -pub struct Computation {} +pub struct Computation { + /// Will fade in one by one. + examples: Vec<ComputationExample>, +} + +/// One bullet point and texture combination. +struct ComputationExample { + /// Bullet point text. + label: &'static str, + /// Texture handle to render on the right side. + texture: TextureHandle, + /// When we started fading it ([`None`] if we haven't started yet). + fade_start: Option<f64>, +} + +impl ComputationExample { + pub fn new(label: &'static str, texture: TextureHandle) -> Self { + Self { + label, + texture, + fade_start: None, + } + } +} impl Slide for Computation { + fn transition(&mut self, ctx: &Context) -> bool { + for example in &mut self.examples { + if example.fade_start.is_none() { + // If any image has not yet started fading in, fade it in and don't go to next slide. + example.fade_start = Some(ctx.input().time); + return false; + } + } + true + } + fn show(&mut self, ui: &mut Ui) { + if self.examples.is_empty() { + // For now, these images are somewhat like placeholders. + self.examples = vec![ + ComputationExample::new("Raytracing", ctx_img!(ui.ctx(), "raytracing0.png")), + ComputationExample::new("Raymarching", ctx_img!(ui.ctx(), "raymarching1.png")), + ComputationExample::new("Particle simulation", ctx_img!(ui.ctx(), "atom0.png")), + ComputationExample::new("Fluid simulation", ctx_img!(ui.ctx(), "fluid0.png")), + ] + } + + const IMAGE_SCALE: f32 = 256.0; + let window_width = ui.available_width(); + let window_height = ui.available_height(); + let window_width_per_image = window_width / self.examples.len() as f32; + Frame::none().margin(Margin::same(20.0)).show(ui, |ui| { ui.heading("More Computation-based Art"); ui.add_space(8.0); - ui.label(" âµ Raytracing"); - ui.label(" âµ Raymarching"); - ui.label(" âµ Particle simulations"); - ui.label(" âµ Fluid simulations"); + for (i, example) in self.examples.iter().enumerate() { + if let Some(fade_start) = example.fade_start { + fade_in(ui, fade_start, |ui| { + ui.label(format!(" âµ {}", example.label)); + }); + fade_in_manual(ui, fade_start, |ui, alpha| { + let position = Pos2::new( + (window_width_per_image - IMAGE_SCALE) * 0.5 + + window_width * (i as f32) / self.examples.len() as f32, + window_height - IMAGE_SCALE * 1.1, + ); + Image::default() + .height(IMAGE_SCALE) + .position(position) + .alpha(alpha) + .show(ui, &example.texture); + }); + } + } }); } }