From fc70bb395592030c572682ca33a555686985c595 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 8 Oct 2024 22:19:22 -0400 Subject: [PATCH] Set up the cross-fade animation --- cyberpunk-slideshow/script.yml | 8 +- cyberpunk-slideshow/src/main.rs | 181 ++++++++++++++++++++++---------- 2 files changed, 129 insertions(+), 60 deletions(-) diff --git a/cyberpunk-slideshow/script.yml b/cyberpunk-slideshow/script.yml index 36fd3c5..a1b8656 100644 --- a/cyberpunk-slideshow/script.yml +++ b/cyberpunk-slideshow/script.yml @@ -1,18 +1,18 @@ - text: The distinguishing thing about magic is that it includes some kind of personal element. The person who is performing the magic is relevant to the magic. -- Ted Chang, Marie Brennan position: top transition: - secs: 2 + secs: 1 nanos: 0 - text: Any sufficiently advanced technology is indistinguishable from magic. -- Arthur C. Clark. position: middle transition: - secs: 2 + secs: 1 nanos: 0 -- text: Electricity is the closest we get to Magic in this world. +- text: Science is our Magic. position: middle transition: - secs: 2 + secs: 1 nanos: 0 diff --git a/cyberpunk-slideshow/src/main.rs b/cyberpunk-slideshow/src/main.rs index ea2d3c2..2de3d20 100644 --- a/cyberpunk-slideshow/src/main.rs +++ b/cyberpunk-slideshow/src/main.rs @@ -1,5 +1,6 @@ use std::{ cell::RefCell, + collections::HashMap, fs::File, io::Read, ops::Index, @@ -22,7 +23,7 @@ use serde::{Deserialize, Serialize}; const FPS: u64 = 60; const PURPLE: (f64, f64, f64) = (0.7, 0., 1.); -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "lowercase")] enum Position { Top, @@ -72,6 +73,13 @@ impl Index for Script { } } +struct Region { + left: f64, + top: f64, + width: f64, + height: f64, +} + struct Fade { text: String, position: Position, @@ -80,29 +88,10 @@ struct Fade { start_time: Instant, } -/* -impl Fade { - fn render(&self, context: &Context) { - let start = Instant::now(); - } -} -*/ - -/* -impl Transition { - fn render(&self, context: &Context) { - match self { - Transition::Fade(fade) => fade.render(context), - Transition::CrossFade(start, end) => {} - } - } -} -*/ - trait Animation { fn position(&self) -> Position; - fn tick(&self, now: Instant, context: &Context) -> bool; + fn tick(&self, now: Instant, context: &Context, region: Region); } impl Animation for Fade { @@ -110,18 +99,50 @@ impl Animation for Fade { self.position.clone() } - fn tick(&self, now: Instant, context: &Context) -> bool { + fn tick(&self, now: Instant, context: &Context, region: Region) { let total_frames = self.duration.as_secs() * FPS; let alpha_rate: f64 = 1. / total_frames as f64; let frames = (now - self.start_time).as_secs_f64() * FPS as f64; let alpha = alpha_rate * frames as f64; - let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha); let text_display = Text::new(self.text.clone(), context, 32.); + let _ = context.move_to(region.left, region.top + text_display.extents().height()); + let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha); + text_display.draw(); + } +} + +struct CrossFade { + old_text: String, + new_text: String, + position: Position, + duration: Duration, + + start_time: Instant, +} + +impl Animation for CrossFade { + fn position(&self) -> Position { + self.position.clone() + } + + fn tick(&self, now: Instant, context: &Context, region: Region) { + let total_frames = self.duration.as_secs() * FPS; + let alpha_rate: f64 = 1. / total_frames as f64; + + let frames = (now - self.start_time).as_secs_f64() * FPS as f64; + let alpha = alpha_rate * frames as f64; + + let text_display = Text::new(self.old_text.clone(), context, 32.); + let _ = context.move_to(region.left, region.top + text_display.extents().height()); + let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, 1. - alpha); text_display.draw(); - false + let text_display = Text::new(self.new_text.clone(), context, 32.); + let _ = context.move_to(region.left, region.top + text_display.extents().height()); + let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha); + text_display.draw(); } } @@ -153,7 +174,7 @@ impl CyberScreenState { s } - fn next_page(&mut self) -> impl Animation { + fn next_page(&mut self) -> Box { let idx = match self.idx { None => 0, Some(idx) => { @@ -167,22 +188,35 @@ impl CyberScreenState { self.idx = Some(idx); let step = self.script[idx].clone(); - match step.position { + let (old, new) = match step.position { Position::Top => { - self.top = Some(step.clone()); + let old = self.top.replace(step.clone()); + (old, step) } Position::Middle => { - self.middle = Some(step.clone()); + let old = self.middle.replace(step.clone()); + (old, step) } Position::Bottom => { - self.bottom = Some(step.clone()); + let old = self.middle.replace(step.clone()); + (old, step) } - } - Fade { - text: step.text.clone(), - position: step.position, - duration: step.transition, - start_time: Instant::now(), + }; + + match old { + Some(old) => Box::new(CrossFade { + old_text: old.text.clone(), + new_text: new.text.clone(), + position: new.position, + duration: new.transition, + start_time: Instant::now(), + }), + None => Box::new(Fade { + text: new.text.clone(), + position: new.position, + duration: new.transition, + start_time: Instant::now(), + }), } } } @@ -190,7 +224,9 @@ impl CyberScreenState { #[derive(Default)] pub struct CyberScreenPrivate { state: Rc>, - animations: Rc>>>, + // For crossfading to work, I have to detect that there is an old animation in a position, and + // replace it with the new one. + animations: Rc>>>, } #[glib::object_subclass] @@ -211,7 +247,9 @@ impl CyberScreenPrivate { fn next_page(&self) { let transition = self.state.borrow_mut().next_page(); - self.animations.borrow_mut().push(Box::new(transition)); + self.animations + .borrow_mut() + .insert(transition.position(), transition); } } @@ -239,27 +277,56 @@ impl CyberScreen { } pen.stroke(); */ + let line = AsymLineCutout { + orientation: gtk::Orientation::Horizontal, + start_x: 25., + start_y: height as f64 / 7., + start_length: width as f64 / 3., + cutout_length: width as f64 / 3. - 25., + height: 25., + end_length: width as f64 / 3. - 50., + invert: false, + }.draw(&pen); + pen.stroke(); let tracery = pen.finish(); let _ = context.set_source(tracery); let _ = context.paint(); let mut animations = s.imp().animations.borrow_mut(); - let mut to_remove = vec![]; - for (idx, animation) in animations.iter().enumerate() { - let y = match animation.position() { - Position::Top => height as f64 * 1. / 5., - Position::Middle => height as f64 * 2. / 5., - Position::Bottom => height as f64 * 3. / 5., - }; - context.move_to(20., y); - let done = animation.tick(now, context); - if done { - to_remove.push(idx) - }; - } - for idx in to_remove.into_iter() { - animations.remove(idx); + let lr_margin = 50.; + let max_width = width as f64 - lr_margin * 2.; + let region_height = height as f64 / 5.; + + if let Some(animation) = animations.get(&Position::Top) { + let y = height as f64 * 1. / 5.; + let region = Region { + left: 20., + top: y, + height: region_height, + width: max_width, + }; + animation.tick(now, context, region); + } + if let Some(animation) = animations.get(&Position::Middle) { + let y = height as f64 * 2. / 5.; + let region = Region { + left: 20., + top: y, + height: region_height, + width: max_width, + }; + animation.tick(now, context, region); + } + if let Some(animation) = animations.get(&Position::Bottom) { + let y = height as f64 * 3. / 5.; + let region = Region { + left: 20., + top: y, + height: region_height, + width: max_width, + }; + animation.tick(now, context, region); } } }); @@ -316,10 +383,12 @@ fn main() { let _ = glib::spawn_future_local({ let screen = screen.clone(); - async move { loop { - screen.queue_draw(); - async_std::task::sleep(Duration::from_millis(1000 / FPS)).await; - }} + async move { + loop { + screen.queue_draw(); + async_std::task::sleep(Duration::from_millis(1000 / FPS)).await; + } + } }); });