From f068f4efe5000584fa93ca285ca060592e6c52f7 Mon Sep 17 00:00:00 2001
From: Finn Bear <finnbearlabs@gmail.com>
Date: Wed, 27 Apr 2022 19:42:11 -0700
Subject: [PATCH] Fade in transition, content WIP.

---
 src/color.rs                        |  68 ++++++++++++
 src/component/code.rs               |  29 ++++--
 src/component/grid.rs               |  29 ++++--
 src/fade_in.rs                      |  17 +++
 src/main.rs                         |  19 +++-
 src/slide.rs                        |   4 +-
 src/slide/s1_title.rs               |   9 +-
 src/slide/s2_introduction.rs        |  46 ++++++---
 src/slide/s3_complexity.rs          |  12 ++-
 src/slide/s4_automata.rs            |  10 +-
 src/slide/s5_fractals.rs            | 154 ++++++++++++++++++----------
 src/slide/s5_fractals/circle.rs     |   3 +
 src/slide/s5_fractals/mandelbrot.rs |  15 +++
 src/slide/s5_fractals/rectangle.rs  |   3 +
 src/slide/s6_computation.rs         |   4 +-
 src/slide/s7_mosaic.rs              |   4 +-
 src/slide/s8_conclusion.rs          |   4 +-
 17 files changed, 318 insertions(+), 112 deletions(-)
 create mode 100644 src/color.rs
 create mode 100644 src/fade_in.rs
 create mode 100644 src/slide/s5_fractals/circle.rs
 create mode 100644 src/slide/s5_fractals/mandelbrot.rs
 create mode 100644 src/slide/s5_fractals/rectangle.rs

diff --git a/src/color.rs b/src/color.rs
new file mode 100644
index 0000000..54b6d31
--- /dev/null
+++ b/src/color.rs
@@ -0,0 +1,68 @@
+use crate::egui::style::WidgetVisuals;
+use eframe::egui::{Color32, InnerResponse, Style, Ui};
+
+/// Reduces the alpha of a color (how opaque it is) by a factor.
+pub fn set_alpha(color: Color32, factor: f32) -> Color32 {
+    if factor == 1.0 {
+        // Fast/common path.
+        color
+    } else {
+        color.linear_multiply(factor)
+    }
+}
+
+/// Same as [`multiply_alpha`], but operates in place.
+pub fn set_alpha_in_place(color: &mut Color32, factor: f32) {
+    assert!(factor >= 0.0 && factor <= 1.0);
+    *color = set_alpha(*color, factor);
+}
+
+/// Changes alpha of entire widget style.
+fn set_widget_alpha_in_place(widget: &mut WidgetVisuals, factor: f32) {
+    set_alpha_in_place(&mut widget.bg_fill, factor);
+    set_alpha_in_place(&mut widget.bg_stroke.color, factor);
+    set_alpha_in_place(&mut widget.fg_stroke.color, factor);
+}
+
+/// Changes alpha of entire style.
+pub fn set_style_alpha(style: &Style, factor: f32) -> Style {
+    let mut clone = style.clone();
+    set_style_alpha_in_place(&mut clone, factor);
+    clone
+}
+
+/// Changes alpha of entire style, in place.
+pub fn set_style_alpha_in_place(style: &mut Style, factor: f32) {
+    if factor == 1.0 {
+        // Common/fast path.
+        return;
+    }
+    let widgets = &mut style.visuals.widgets;
+    for widget in [
+        &mut widgets.noninteractive,
+        &mut widgets.inactive,
+        &mut widgets.hovered,
+        &mut widgets.active,
+        &mut widgets.open,
+    ] {
+        set_widget_alpha_in_place(widget, factor);
+    }
+    set_alpha_in_place(&mut style.visuals.hyperlink_color, factor);
+    set_alpha_in_place(&mut style.visuals.faint_bg_color, factor);
+    set_alpha_in_place(&mut style.visuals.extreme_bg_color, factor);
+    set_alpha_in_place(&mut style.visuals.code_bg_color, factor);
+    set_alpha_in_place(&mut style.visuals.window_shadow.color, factor);
+
+    //style.visuals.widgets.noninteractive.bg_fill = Color32::TRANSPARENT;
+}
+
+pub fn with_alpha<R>(
+    ui: &mut Ui,
+    alpha: f32,
+    add_contents: impl FnOnce(&mut Ui) -> R,
+) -> InnerResponse<R> {
+    ui.scope(|ui| {
+        set_style_alpha_in_place(ui.style_mut(), alpha);
+        add_contents(ui)
+    })
+}
diff --git a/src/component/code.rs b/src/component/code.rs
index 2202d1b..dc830e1 100644
--- a/src/component/code.rs
+++ b/src/component/code.rs
@@ -1,5 +1,6 @@
+use crate::color::{set_alpha_in_place, set_style_alpha};
 use eframe::egui;
-use eframe::egui::{Align2, Color32, Context, Vec2, Widget, Window};
+use eframe::egui::{Align2, Color32, Context, Frame, Vec2, Widget, Window};
 
 #[derive(Clone)]
 pub struct Code {
@@ -13,6 +14,8 @@ pub struct Code {
     pub size_pixels: f32,
     /// Background color. Transparent disables background.
     pub background: Color32,
+    /// Alpha (how opaque) entire widget is.
+    pub alpha: f32,
     /// Code.
     pub code: String,
 }
@@ -25,6 +28,7 @@ impl Default for Code {
             offset_y: 0.0,
             size_pixels: 400.0,
             background: Color32::from_rgb(230, 230, 230),
+            alpha: 1.0,
             code: String::from(r#"println!("Hello world!");"#),
         }
     }
@@ -35,17 +39,15 @@ impl Code {
     pub fn show(&self, ctx: &Context) {
         Window::new(self.name)
             .title_bar(false)
-            //.frame(Frame::none())
+            .frame(Frame::window(&set_style_alpha(&ctx.style(), self.alpha)))
             .anchor(
                 Align2::CENTER_CENTER,
                 Vec2::new(self.offset_x, self.offset_y),
             )
             .fixed_size(Vec2::splat(self.size_pixels))
             .show(ctx, |ui| {
-                //let (_id, _rect) = ui.allocate_space(Vec2::splat(self.size_pixels));
-
+                // Syntax coloring code adapted from: https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/code_editor.rs
                 let theme = egui_demo_lib::syntax_highlighting::CodeTheme::light();
-
                 let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
                     let mut layout_job = egui_demo_lib::syntax_highlighting::highlight(
                         ui.ctx(),
@@ -53,6 +55,9 @@ impl Code {
                         string,
                         "rs",
                     );
+                    for section in &mut layout_job.sections {
+                        set_alpha_in_place(&mut section.format.color, self.alpha);
+                    }
                     layout_job.wrap_width = wrap_width;
                     ui.fonts().layout_job(layout_job)
                 };
@@ -60,7 +65,6 @@ impl Code {
                 egui::TextEdit::multiline(&mut self.code.as_str())
                     .font(egui::TextStyle::Monospace) // Only affects cursor.
                     .code_editor()
-                    //.desired_rows(10)
                     .lock_focus(true)
                     .desired_width(f32::INFINITY)
                     .layouter(&mut layouter)
@@ -68,3 +72,16 @@ impl Code {
             });
     }
 }
+
+/// Converts Rust code to pseudocode.
+///
+/// Note: Must use non-idiomatic `return x;` instead of just `x`.
+pub fn pseudocode(code: &str) -> String {
+    code.replace("pub ", "")
+        .replace("fn ", "function ")
+        .replace("let ", "")
+        .replace("mut ", "")
+        .replace(": f32", "")
+        .replace(" -> bool", "")
+        .replace(";\n", "\n")
+}
diff --git a/src/component/grid.rs b/src/component/grid.rs
index fe1144e..499e410 100644
--- a/src/component/grid.rs
+++ b/src/component/grid.rs
@@ -1,3 +1,5 @@
+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::epaint::RectShape;
@@ -20,6 +22,8 @@ pub struct Grid {
     pub stroke_width: f32,
     /// Background color. Transparent disables background.
     pub background: Color32,
+    /// Transparency of entire widget.
+    pub alpha: f32,
     /// Fill color of each pixel (use getters and setters).
     fill: Vec<Color32>,
 }
@@ -35,6 +39,7 @@ impl Default for Grid {
             stroke: Color32::GRAY,
             stroke_width: 1.0,
             background: Color32::from_rgb(230, 230, 230),
+            alpha: 1.0,
             fill: Vec::new(),
         }
     }
@@ -101,7 +106,7 @@ impl Grid {
     pub fn show(&self, ctx: &Context) {
         Window::new(self.name)
             .title_bar(false)
-            //.frame(Frame::none())
+            .frame(Frame::window(&set_style_alpha(&ctx.style(), self.alpha)))
             .anchor(
                 Align2::CENTER_CENTER,
                 Vec2::new(self.offset_x, self.offset_y),
@@ -114,37 +119,41 @@ impl Grid {
                     rect,
                 );
 
-                if self.background.a() > 0 {
+                let background = set_alpha(self.background, self.alpha);
+                if background.a() > 0 {
                     ui.painter().add(Shape::Rect(RectShape::filled(
                         to_screen.transform_rect(Rect::from_x_y_ranges(0f32..=1f32, 0f32..=1f32)),
                         Rounding::none(),
-                        self.background,
+                        background,
                     )));
                 }
 
                 let cell_width = 1.0 / self.size_cells as f32;
+                // Make sure neighboring cells overlap without gaps.
+                let tolerance = 0.001;
                 for y in 0..self.size_cells {
                     let y_coord = y as f32 / self.size_cells as f32;
                     for x in 0..self.size_cells {
                         let idx = self.index(x, y);
                         if let Some(fill) = self.fill.get(idx) {
+                            let fill = set_alpha(*fill, self.alpha);
                             if fill.a() > 0 {
                                 let x_coord = x as f32 / self.size_cells as f32;
-                                const TOLERANCE: f32 = 0.001;
                                 ui.painter().add(Shape::Rect(RectShape::filled(
                                     to_screen.transform_rect(Rect::from_x_y_ranges(
-                                        x_coord - TOLERANCE..=x_coord + cell_width + TOLERANCE,
-                                        y_coord - TOLERANCE..=y_coord + cell_width + TOLERANCE,
+                                        x_coord - tolerance..=x_coord + cell_width + tolerance,
+                                        y_coord - tolerance..=y_coord + cell_width + tolerance,
                                     )),
                                     Rounding::none(),
-                                    *fill,
+                                    fill,
                                 )));
                             }
                         }
                     }
                 }
 
-                if self.stroke.a() > 0 && self.stroke_width > 0.0 {
+                let stroke = set_alpha(self.stroke, self.alpha);
+                if stroke.a() > 0 && self.stroke_width > 0.0 {
                     for c in 0..=self.size_cells {
                         let coord = c as f32 / self.size_cells as f32;
                         // Horizontal.
@@ -153,7 +162,7 @@ impl Grid {
                                 to_screen * Pos2::new(0.0, coord),
                                 to_screen * Pos2::new(1.0, coord),
                             ],
-                            stroke: Stroke::new(self.stroke_width, self.stroke),
+                            stroke: Stroke::new(self.stroke_width, stroke),
                         });
                         // Vertical.
                         ui.painter().add(Shape::LineSegment {
@@ -161,7 +170,7 @@ impl Grid {
                                 to_screen * Pos2::new(coord, 0.0),
                                 to_screen * Pos2::new(coord, 1.0),
                             ],
-                            stroke: Stroke::new(self.stroke_width, self.stroke),
+                            stroke: Stroke::new(self.stroke_width, stroke),
                         });
                     }
                 }
diff --git a/src/fade_in.rs b/src/fade_in.rs
new file mode 100644
index 0000000..a515e5c
--- /dev/null
+++ b/src/fade_in.rs
@@ -0,0 +1,17 @@
+use crate::color::with_alpha;
+use crate::egui::{InnerResponse, Ui};
+
+const FADE_DURATION: f64 = 0.6;
+
+pub fn fade_in<R>(
+    ui: &mut Ui,
+    fade_start: f64,
+    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;
+    if alpha < 1.0 {
+        ui.ctx().request_repaint();
+    }
+    with_alpha(ui, alpha, |ui| add_contents(ui))
+}
diff --git a/src/main.rs b/src/main.rs
index 8dd7e1c..82c522b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,11 +1,14 @@
 #![feature(derive_default_enum)]
 #![feature(mixed_integer_ops)]
 
+pub mod color;
 pub mod component;
+pub mod fade_in;
 pub mod governor;
 pub mod image;
 pub mod slide;
 
+use crate::fade_in::fade_in;
 use crate::slide::s1_title::Title;
 use crate::slide::s2_introduction::Introduction;
 use crate::slide::s3_complexity::Complexity;
@@ -32,10 +35,10 @@ fn main() {
         drag_and_drop_support: false,
         icon_data: None,
         initial_window_pos: None,
-        initial_window_size: None,
-        min_window_size: Some(size),
+        initial_window_size: Some(size),
+        min_window_size: None,
         max_window_size: Some(size),
-        resizable: false,
+        resizable: true,
         transparent: false,
     };
 
@@ -45,6 +48,7 @@ fn main() {
 pub struct Cartoon {
     slides: Vec<Box<dyn Slide>>,
     slide_index: usize,
+    transition_time: f64,
 }
 
 impl Default for Cartoon {
@@ -52,6 +56,7 @@ impl Default for Cartoon {
         Self {
             slides: create_slides(),
             slide_index: 0,
+            transition_time: 0.0,
         }
     }
 }
@@ -98,6 +103,8 @@ impl epi::App for Cartoon {
         }
          */
         ctx.set_style(style);
+
+        self.transition_time = ctx.input().time;
     }
 
     /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
@@ -128,6 +135,7 @@ impl epi::App for Cartoon {
                     if new == 0 {
                         self.slides = create_slides();
                     }
+                    self.transition_time = ctx.input().time;
                     ctx.request_repaint();
                     new
                 } else {
@@ -135,6 +143,7 @@ impl epi::App for Cartoon {
                 }
             } else {
                 ctx.request_repaint();
+                self.transition_time = ctx.input().time;
                 self.slide_index
                     .checked_sub(1)
                     .unwrap_or(self.slides.len() - 1)
@@ -142,7 +151,9 @@ impl epi::App for Cartoon {
         }
 
         egui::CentralPanel::default().show(ctx, |ui| {
-            self.slides[self.slide_index].show(ui, ctx);
+            fade_in(ui, self.transition_time, |ui| {
+                self.slides[self.slide_index].show(ui);
+            });
         });
     }
 }
diff --git a/src/slide.rs b/src/slide.rs
index 0b69fef..b2f7a81 100644
--- a/src/slide.rs
+++ b/src/slide.rs
@@ -10,10 +10,12 @@ pub mod s7_mosaic;
 pub mod s8_conclusion;
 
 pub trait Slide {
-    fn show(&mut self, ui: &mut Ui, ctx: &Context);
     /// Returns whether "done."
     /// Repaint automatically requested.
     fn transition(&mut self, _ctx: &Context) -> bool {
         true
     }
+
+    /// Renders slide into UI.
+    fn show(&mut self, ui: &mut Ui);
 }
diff --git a/src/slide/s1_title.rs b/src/slide/s1_title.rs
index acc37a5..947ecd2 100644
--- a/src/slide/s1_title.rs
+++ b/src/slide/s1_title.rs
@@ -1,4 +1,4 @@
-use crate::egui::{Align2, Context};
+use crate::egui::Align2;
 use crate::{ctx_img, img, Margin, Slide};
 use eframe::egui;
 use eframe::egui::{Frame, Ui, Vec2, Window};
@@ -9,7 +9,7 @@ pub struct Title {
 }
 
 impl Slide for Title {
-    fn show(&mut self, ui: &mut Ui, ctx: &Context) {
+    fn show(&mut self, ui: &mut Ui) {
         if self.examples.is_empty() {
             self.examples = vec![
                 ctx_img!(ui.ctx(), "raymarching0.png"),
@@ -28,7 +28,7 @@ impl Slide for Title {
                     .title_bar(false)
                     .resizable(false)
                     .frame(Frame::window(&ui.style()).margin(Margin::same(5.0)))
-                    .show(ctx, |ui| {
+                    .show(ui.ctx(), |ui| {
                         ui.image(example, Vec2::splat(128.0));
                     });
             }
@@ -39,7 +39,8 @@ impl Slide for Title {
             .resizable(false)
             .anchor(Align2::CENTER_CENTER, Vec2::ZERO)
             .default_width(400.0)
-            .show(ctx, |ui| {
+            .frame(Frame::window(ui.style()))
+            .show(ui.ctx(), |ui| {
                 ui.vertical_centered(|ui| {
                     ui.heading("Generative Art");
                     ui.label("By: Finn, Matthew, Nathan, Owen");
diff --git a/src/slide/s2_introduction.rs b/src/slide/s2_introduction.rs
index 56753da..fe97817 100644
--- a/src/slide/s2_introduction.rs
+++ b/src/slide/s2_introduction.rs
@@ -1,19 +1,34 @@
-use crate::{Margin, Slide};
+use crate::{fade_in, Margin, Slide};
 use eframe::egui::{Context, Frame, Ui};
 
 #[derive(Default)]
 pub struct Introduction {
-    strengths: bool,
+    state: IntroductionState,
+}
+
+#[derive(Default)]
+enum IntroductionState {
+    #[default]
+    Weaknesses,
+    Strengths {
+        transition_time: f64,
+    },
 }
 
 impl Slide for Introduction {
-    fn transition(&mut self, _ctx: &Context) -> bool {
-        let done = self.strengths;
-        self.strengths = true;
-        done
+    fn transition(&mut self, ctx: &Context) -> bool {
+        match self.state {
+            IntroductionState::Weaknesses => {
+                self.state = IntroductionState::Strengths {
+                    transition_time: ctx.input().time,
+                };
+                false
+            }
+            IntroductionState::Strengths { .. } => true,
+        }
     }
 
-    fn show(&mut self, ui: &mut Ui, _ctx: &Context) {
+    fn show(&mut self, ui: &mut Ui) {
         Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
             ui.heading("Introduction to Artistic Algorithms");
             ui.add_space(8.0);
@@ -21,13 +36,16 @@ impl Slide for Introduction {
             ui.small(" ✖ Social context");
             ui.small(" ✖ Human emotion");
             ui.small(" ✖ Political commentary");
-            if self.strengths {
-                ui.add_space(10.0);
-                ui.label("Strengths");
-                ui.small(" ✔ Following rules");
-                ui.small(" ✔ Performing computation");
-                ui.small(" ✔ Harnessing chaos and randomness");
-                ui.small(" ✔ Guided exploration");
+
+            if let IntroductionState::Strengths { transition_time } = &self.state {
+                fade_in(ui, *transition_time, |ui| {
+                    ui.add_space(10.0);
+                    ui.label("Strengths");
+                    ui.small(" ✔ Following rules");
+                    ui.small(" ✔ Performing computation");
+                    ui.small(" ✔ Harnessing chaos and randomness");
+                    ui.small(" ✔ Guided exploration");
+                });
             }
         });
     }
diff --git a/src/slide/s3_complexity.rs b/src/slide/s3_complexity.rs
index c208e48..92e09ce 100644
--- a/src/slide/s3_complexity.rs
+++ b/src/slide/s3_complexity.rs
@@ -1,7 +1,7 @@
 use crate::egui::{Context, Ui, Window};
 use crate::Slide;
 use eframe::egui::text_edit::{CCursorRange, TextEditState};
-use eframe::egui::{Align2, Frame, TextEdit, Vec2};
+use eframe::egui::{Align2, Frame, TextEdit, Vec2, Widget};
 use eframe::epaint::text::cursor::CCursor;
 
 #[derive(Default)]
@@ -29,14 +29,14 @@ impl Slide for Complexity {
         }
     }
 
-    fn show(&mut self, _ui: &mut Ui, ctx: &Context) {
+    fn show(&mut self, ui: &mut Ui) {
         Window::new("complexity")
             .title_bar(false)
             .resizable(false)
             .frame(Frame::none())
             .anchor(Align2::CENTER_CENTER, Vec2::ZERO)
             .fixed_size(Vec2::new(440.0, 100.0))
-            .show(ctx, |ui| {
+            .show(ui.ctx(), |ui| {
                 ui.ctx().request_repaint();
                 let time = ui.input().time;
                 let time_delta = (time - self.last_time).min(1.0);
@@ -81,13 +81,15 @@ impl Slide for Complexity {
                         }
                     }
 
-                    let text_edit = ui.text_edit_singleline(&mut text.as_str());
+                    let text_edit = TextEdit::singleline(&mut text.as_str())
+                        .desired_width(f32::INFINITY)
+                        .ui(ui);
                     if state.ccursor_range().is_some() {
                         text_edit.request_focus();
                     } else {
                         text_edit.surrender_focus();
                     }
-                    TextEdit::store_state(ctx, text_edit.id, state);
+                    TextEdit::store_state(ui.ctx(), text_edit.id, state);
                 });
             });
     }
diff --git a/src/slide/s4_automata.rs b/src/slide/s4_automata.rs
index 67b625c..5ba5385 100644
--- a/src/slide/s4_automata.rs
+++ b/src/slide/s4_automata.rs
@@ -2,7 +2,7 @@ use crate::component::grid::Grid;
 use crate::governor::Governor;
 use crate::Slide;
 use eframe::egui::style::Margin;
-use eframe::egui::{Color32, Context, Frame, Ui};
+use eframe::egui::{Color32, Frame, Ui};
 use rand::Rng;
 
 pub struct Automata {
@@ -37,18 +37,18 @@ impl Default for Automata {
 }
 
 impl Slide for Automata {
-    fn show(&mut self, ui: &mut Ui, ctx: &Context) {
+    fn show(&mut self, ui: &mut Ui) {
         Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
             ui.vertical_centered(|ui| {
                 ui.heading("Cellular Automata");
             });
         });
 
-        ctx.request_repaint();
-        if self.governor.ready(ctx.input().time, 0.2) {
+        ui.ctx().request_repaint();
+        if self.governor.ready(ui.ctx().input().time, 0.2) {
             self.life = conways_game_of_life(&self.life);
         }
-        self.life.show(ctx);
+        self.life.show(ui.ctx());
     }
 }
 
diff --git a/src/slide/s5_fractals.rs b/src/slide/s5_fractals.rs
index cc58e7c..037baa4 100644
--- a/src/slide/s5_fractals.rs
+++ b/src/slide/s5_fractals.rs
@@ -1,52 +1,120 @@
-use crate::component::code::Code;
+mod circle;
+mod mandelbrot;
+mod rectangle;
+
+use crate::component::code::{pseudocode, Code};
 use crate::component::grid::Grid;
 use crate::egui::{Color32, Context, Frame, Ui};
+use crate::slide::s5_fractals::circle::circle;
+use crate::slide::s5_fractals::mandelbrot::mandelbrot;
+use crate::slide::s5_fractals::rectangle::rectangle;
 use crate::Slide;
 use eframe::egui::style::Margin;
 
 pub struct Fractals {
+    state: FractalsState,
     code: Code,
     grid: Grid,
 }
 
+#[derive(Default)]
+#[allow(unused)]
+enum FractalsState {
+    #[default]
+    Before,
+    Grid {
+        fade_start: f64,
+    },
+    Rectangle {
+        pixel: usize,
+    },
+    Circle {
+        pixel: usize,
+    },
+    Mandelbrot {
+        pixel: usize,
+    },
+}
+
 impl Default for Fractals {
     fn default() -> Self {
         const HORIZONTAL_OFFSET: f32 = 275.0;
 
         let mut code = Code::default();
+        code.name = "fractal_code";
+        code.offset_x = -HORIZONTAL_OFFSET;
 
-        code.code = String::from(
-            r###"
-fn mandelbrot(x0, y0) -> bool {
-    MAX = 512
-    x = 0.0
-    y = 0.0
-    i = 0
-
-    while x^2 + y^2 <= 4 and i < MAX {
-        x_temp = x^2 - y^2 + x0
-        y = 2.0 * x * y + y0
-        x = x_temp
-        i += 1
-    }
+        let mut grid = Grid::default();
+        grid.name = "fractal_grid";
+        grid.offset_x = HORIZONTAL_OFFSET;
 
-    return i == MAX
+        grid.size_cells = 256;
+        grid.stroke_width = 0.0;
+
+        Self {
+            state: FractalsState::default(),
+            code,
+            grid,
+        }
+    }
 }
-"###,
-        );
 
-        code.offset_x = -HORIZONTAL_OFFSET;
+impl Slide for Fractals {
+    fn transition(&mut self, ctx: &Context) -> bool {
+        // Increment state by one.
+        match self.state {
+            FractalsState::Before => {
+                self.state = FractalsState::Grid {
+                    fade_start: ctx.input().time,
+                }
+            }
+            FractalsState::Grid { .. } => self.state = FractalsState::Rectangle { pixel: 0 },
+            FractalsState::Rectangle { .. } => self.state = FractalsState::Circle { pixel: 0 },
+            FractalsState::Circle { .. } => self.state = FractalsState::Mandelbrot { pixel: 0 },
+            FractalsState::Mandelbrot { .. } => return true,
+        }
+        false
+    }
 
-        let mut grid = Grid::default();
+    fn show(&mut self, ui: &mut Ui) {
+        Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
+            ui.vertical_centered(|ui| {
+                ui.heading("Fractals");
+            });
+        });
 
-        const RESOLUTION: usize = 256;
+        ui.ctx().request_repaint();
+
+        let algo: Box<dyn Fn(f32, f32) -> bool>;
+
+        match self.state {
+            FractalsState::Before => {
+                return;
+            }
+            FractalsState::Grid { fade_start } => {
+                let elapsed = ui.ctx().input().time - fade_start;
+                let alpha = (elapsed * 2.0).min(1.0) as f32;
+                self.code.alpha = alpha;
+                self.grid.alpha = alpha;
+                algo = Box::new(|_, _| false);
+            }
+            FractalsState::Rectangle { .. } => {
+                self.code.code = pseudocode(include_str!("s5_fractals/rectangle.rs"));
+                algo = Box::new(rectangle);
+            }
+            FractalsState::Circle { .. } => {
+                self.code.code = pseudocode(include_str!("s5_fractals/circle.rs"));
+                algo = Box::new(circle);
+            }
+            FractalsState::Mandelbrot { .. } => {
+                self.code.code = pseudocode(include_str!("s5_fractals/mandelbrot.rs"));
+                algo = Box::new(mandelbrot);
+            }
+        }
 
-        grid.name = "fractal";
-        grid.size_cells = RESOLUTION;
-        grid.stroke_width = 0.0;
-        grid.offset_x = HORIZONTAL_OFFSET;
+        const RESOLUTION: usize = 256;
 
-        for (gx, gy, c) in grid.iter_mut() {
+        for (gx, gy, c) in self.grid.iter_mut() {
             // 0 to 1.
             let nx = (gx as f32 + 0.5) / RESOLUTION as f32;
             let ny = (gy as f32 + 0.5) / RESOLUTION as f32;
@@ -55,42 +123,14 @@ fn mandelbrot(x0, y0) -> bool {
             let x = nx * 4.0 - 2.0;
             let y = ny * 4.0 - 2.0;
 
-            *c = if mandelbrot(x, y) {
+            *c = if algo(x, y) {
                 Color32::BLACK
             } else {
                 Color32::TRANSPARENT
             };
         }
 
-        Self { code, grid }
-    }
-}
-
-impl Slide for Fractals {
-    fn show(&mut self, ui: &mut Ui, ctx: &Context) {
-        Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
-            ui.vertical_centered(|ui| {
-                ui.heading("Fractals");
-            });
-        });
-
-        self.code.show(ctx);
-        self.grid.show(ctx);
+        self.code.show(ui.ctx());
+        self.grid.show(ui.ctx());
     }
 }
-
-fn mandelbrot(x0: f32, y0: f32) -> bool {
-    const MAX_ITERATIONS: usize = 512;
-
-    let mut x = 0f32;
-    let mut y = 0f32;
-    let mut i = 0usize;
-    while x.powi(2) + y.powi(2) <= 2f32.powi(2) && i < MAX_ITERATIONS {
-        let x_tmp = x.powi(2) - y.powi(2) + x0;
-        y = 2.0 * x * y + y0;
-        x = x_tmp;
-        i += 1;
-    }
-
-    i == MAX_ITERATIONS
-}
diff --git a/src/slide/s5_fractals/circle.rs b/src/slide/s5_fractals/circle.rs
new file mode 100644
index 0000000..2ede0c0
--- /dev/null
+++ b/src/slide/s5_fractals/circle.rs
@@ -0,0 +1,3 @@
+pub fn circle(x: f32, y: f32) -> bool {
+    return x * x + y * y <= 3.0;
+}
diff --git a/src/slide/s5_fractals/mandelbrot.rs b/src/slide/s5_fractals/mandelbrot.rs
new file mode 100644
index 0000000..ffa1715
--- /dev/null
+++ b/src/slide/s5_fractals/mandelbrot.rs
@@ -0,0 +1,15 @@
+pub fn mandelbrot(x: f32, y: f32) -> bool {
+    let max = 512;
+    let mut x1 = 0.0;
+    let mut y1 = 0.0;
+    let mut i = 0;
+
+    while x1 * x1 + y1 * y1 <= 4.0 && i < max {
+        let x_tmp = x1 * x1 - y1 * y1 + x;
+        y1 = 2.0 * x1 * y1 + y;
+        x1 = x_tmp;
+        i += 1;
+    }
+
+    return i == max;
+}
diff --git a/src/slide/s5_fractals/rectangle.rs b/src/slide/s5_fractals/rectangle.rs
new file mode 100644
index 0000000..8c6978f
--- /dev/null
+++ b/src/slide/s5_fractals/rectangle.rs
@@ -0,0 +1,3 @@
+pub fn rectangle(x: f32, y: f32) -> bool {
+    return -1.5 < x && x < 1.5 && -1.0 < y && y < 1.0;
+}
diff --git a/src/slide/s6_computation.rs b/src/slide/s6_computation.rs
index 8336531..68a4a05 100644
--- a/src/slide/s6_computation.rs
+++ b/src/slide/s6_computation.rs
@@ -1,4 +1,4 @@
-use crate::egui::{Context, Frame, Ui};
+use crate::egui::{Frame, Ui};
 use crate::Slide;
 use eframe::egui::style::Margin;
 
@@ -6,7 +6,7 @@ use eframe::egui::style::Margin;
 pub struct Computation {}
 
 impl Slide for Computation {
-    fn show(&mut self, ui: &mut Ui, _ctx: &Context) {
+    fn show(&mut self, ui: &mut Ui) {
         Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
             ui.heading("More Computation-based Art");
             ui.add_space(8.0);
diff --git a/src/slide/s7_mosaic.rs b/src/slide/s7_mosaic.rs
index d34e839..36241f1 100644
--- a/src/slide/s7_mosaic.rs
+++ b/src/slide/s7_mosaic.rs
@@ -1,4 +1,4 @@
-use crate::egui::{Context, Ui};
+use crate::egui::Ui;
 use crate::Slide;
 use eframe::egui::style::Margin;
 use eframe::egui::Frame;
@@ -7,7 +7,7 @@ use eframe::egui::Frame;
 pub struct Mosaic {}
 
 impl Slide for Mosaic {
-    fn show(&mut self, ui: &mut Ui, _ctx: &Context) {
+    fn show(&mut self, ui: &mut Ui) {
         Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
             ui.heading("Going Further");
             ui.add_space(8.0);
diff --git a/src/slide/s8_conclusion.rs b/src/slide/s8_conclusion.rs
index ea44b7d..169ff0a 100644
--- a/src/slide/s8_conclusion.rs
+++ b/src/slide/s8_conclusion.rs
@@ -1,4 +1,4 @@
-use crate::egui::{Context, Ui};
+use crate::egui::Ui;
 use crate::Slide;
 use eframe::egui::style::Margin;
 use eframe::egui::Frame;
@@ -7,7 +7,7 @@ use eframe::egui::Frame;
 pub struct Conclusion {}
 
 impl Slide for Conclusion {
-    fn show(&mut self, ui: &mut Ui, _ctx: &Context) {
+    fn show(&mut self, ui: &mut Ui) {
         Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
             ui.heading("Conclusion");
             ui.add_space(8.0);
-- 
GitLab