Create a slideshow application in my cyberpunk style #252

Open
savanni wants to merge 10 commits from cybperpunk-billboard into main
2 changed files with 129 additions and 60 deletions
Showing only changes of commit fc70bb3955 - Show all commits

View File

@ -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 - 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 position: top
transition: transition:
secs: 2 secs: 1
nanos: 0 nanos: 0
- text: Any sufficiently advanced technology is indistinguishable from magic. -- Arthur C. Clark. - text: Any sufficiently advanced technology is indistinguishable from magic. -- Arthur C. Clark.
position: middle position: middle
transition: transition:
secs: 2 secs: 1
nanos: 0 nanos: 0
- text: Electricity is the closest we get to Magic in this world. - text: Science is our Magic.
position: middle position: middle
transition: transition:
secs: 2 secs: 1
nanos: 0 nanos: 0

View File

@ -1,5 +1,6 @@
use std::{ use std::{
cell::RefCell, cell::RefCell,
collections::HashMap,
fs::File, fs::File,
io::Read, io::Read,
ops::Index, ops::Index,
@ -22,7 +23,7 @@ use serde::{Deserialize, Serialize};
const FPS: u64 = 60; const FPS: u64 = 60;
const PURPLE: (f64, f64, f64) = (0.7, 0., 1.); 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")] #[serde(rename_all = "lowercase")]
enum Position { enum Position {
Top, Top,
@ -72,6 +73,13 @@ impl Index<usize> for Script {
} }
} }
struct Region {
left: f64,
top: f64,
width: f64,
height: f64,
}
struct Fade { struct Fade {
text: String, text: String,
position: Position, position: Position,
@ -80,29 +88,10 @@ struct Fade {
start_time: Instant, 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 { trait Animation {
fn position(&self) -> Position; 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 { impl Animation for Fade {
@ -110,18 +99,50 @@ impl Animation for Fade {
self.position.clone() 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 total_frames = self.duration.as_secs() * FPS;
let alpha_rate: f64 = 1. / total_frames as f64; let alpha_rate: f64 = 1. / total_frames as f64;
let frames = (now - self.start_time).as_secs_f64() * FPS as f64; let frames = (now - self.start_time).as_secs_f64() * FPS as f64;
let alpha = alpha_rate * frames 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 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(); 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 s
} }
fn next_page(&mut self) -> impl Animation { fn next_page(&mut self) -> Box<dyn Animation> {
let idx = match self.idx { let idx = match self.idx {
None => 0, None => 0,
Some(idx) => { Some(idx) => {
@ -167,22 +188,35 @@ impl CyberScreenState {
self.idx = Some(idx); self.idx = Some(idx);
let step = self.script[idx].clone(); let step = self.script[idx].clone();
match step.position { let (old, new) = match step.position {
Position::Top => { Position::Top => {
self.top = Some(step.clone()); let old = self.top.replace(step.clone());
(old, step)
} }
Position::Middle => { Position::Middle => {
self.middle = Some(step.clone()); let old = self.middle.replace(step.clone());
(old, step)
} }
Position::Bottom => { Position::Bottom => {
self.bottom = Some(step.clone()); let old = self.middle.replace(step.clone());
(old, step)
} }
} };
Fade {
text: step.text.clone(), match old {
position: step.position, Some(old) => Box::new(CrossFade {
duration: step.transition, old_text: old.text.clone(),
new_text: new.text.clone(),
position: new.position,
duration: new.transition,
start_time: Instant::now(), 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)] #[derive(Default)]
pub struct CyberScreenPrivate { pub struct CyberScreenPrivate {
state: Rc<RefCell<CyberScreenState>>, state: Rc<RefCell<CyberScreenState>>,
animations: Rc<RefCell<Vec<Box<dyn Animation>>>>, // 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<RefCell<HashMap<Position, Box<dyn Animation>>>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -211,7 +247,9 @@ impl CyberScreenPrivate {
fn next_page(&self) { fn next_page(&self) {
let transition = self.state.borrow_mut().next_page(); 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(); 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 tracery = pen.finish();
let _ = context.set_source(tracery); let _ = context.set_source(tracery);
let _ = context.paint(); let _ = context.paint();
let mut animations = s.imp().animations.borrow_mut(); 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() { let lr_margin = 50.;
animations.remove(idx); 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 _ = glib::spawn_future_local({
let screen = screen.clone(); let screen = screen.clone();
async move { loop { async move {
loop {
screen.queue_draw(); screen.queue_draw();
async_std::task::sleep(Duration::from_millis(1000 / FPS)).await; async_std::task::sleep(Duration::from_millis(1000 / FPS)).await;
}} }
}
}); });
}); });