Create a slideshow application in my cyberpunk style #252
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue