monorepo/cyberpunk-slideshow/src/main.rs

397 lines
11 KiB
Rust
Raw Normal View History

2024-10-08 03:47:17 +00:00
use std::{
cell::RefCell,
2024-10-09 02:19:22 +00:00
collections::HashMap,
2024-10-08 03:47:17 +00:00
fs::File,
io::Read,
ops::Index,
path::Path,
rc::Rc,
time::{Duration, Instant},
};
2024-10-08 03:47:17 +00:00
use cairo::Context;
2024-10-05 00:56:37 +00:00
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text};
use glib::{GString, Object};
use gtk::{
glib::{self, Propagation},
prelude::*,
subclass::prelude::*,
EventControllerKey,
};
use serde::{Deserialize, Serialize};
2024-10-08 03:47:17 +00:00
const FPS: u64 = 60;
const PURPLE: (f64, f64, f64) = (0.7, 0., 1.);
2024-10-09 02:19:22 +00:00
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
enum Position {
Top,
Middle,
Bottom,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Step {
text: String,
position: Position,
2024-10-08 03:47:17 +00:00
transition: Duration,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Script(Vec<Step>);
impl Script {
fn from_file(path: &Path) -> Result<Script, serde_yml::Error> {
let mut buf: Vec<u8> = Vec::new();
let mut f = File::open(path).unwrap();
f.read_to_end(&mut buf).unwrap();
let script = serde_yml::from_slice(&buf)?;
Ok(Self(script))
}
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a Step> {
self.0.iter()
}
fn len(&self) -> usize {
self.0.len()
}
}
impl Default for Script {
fn default() -> Self {
Self(vec![])
}
}
impl Index<usize> for Script {
type Output = Step;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
2024-10-09 02:19:22 +00:00
struct Region {
left: f64,
top: f64,
width: f64,
height: f64,
}
2024-10-08 03:47:17 +00:00
struct Fade {
text: String,
position: Position,
duration: Duration,
start_time: Instant,
}
trait Animation {
fn position(&self) -> Position;
2024-10-09 02:19:22 +00:00
fn tick(&self, now: Instant, context: &Context, region: Region);
2024-10-08 03:47:17 +00:00
}
impl Animation for Fade {
fn position(&self) -> Position {
self.position.clone()
}
2024-10-09 02:19:22 +00:00
fn tick(&self, now: Instant, context: &Context, region: Region) {
2024-10-08 03:47:17 +00:00
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.text.clone(), context, 32.);
2024-10-09 02:19:22 +00:00
let _ = context.move_to(region.left, region.top + text_display.extents().height());
let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha);
2024-10-08 03:47:17 +00:00
text_display.draw();
2024-10-09 02:19:22 +00:00
}
}
struct CrossFade {
old_text: String,
new_text: String,
position: Position,
duration: Duration,
2024-10-08 03:47:17 +00:00
2024-10-09 02:19:22 +00:00
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();
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();
2024-10-08 03:47:17 +00:00
}
}
#[derive(Debug)]
pub struct CyberScreenState {
script: Script,
idx: Option<usize>,
top: Option<Step>,
middle: Option<Step>,
bottom: Option<Step>,
}
impl Default for CyberScreenState {
fn default() -> Self {
Self {
script: Script(vec![]),
idx: None,
top: None,
middle: None,
bottom: None,
}
}
}
impl CyberScreenState {
fn new(script: Script) -> CyberScreenState {
let mut s = CyberScreenState::default();
s.script = script;
s
}
2024-10-09 02:19:22 +00:00
fn next_page(&mut self) -> Box<dyn Animation> {
let idx = match self.idx {
None => 0,
2024-10-08 03:47:17 +00:00
Some(idx) => {
if idx < self.script.len() {
idx + 1
} else {
idx
}
}
};
self.idx = Some(idx);
let step = self.script[idx].clone();
2024-10-09 02:19:22 +00:00
let (old, new) = match step.position {
Position::Top => {
2024-10-09 02:19:22 +00:00
let old = self.top.replace(step.clone());
(old, step)
}
Position::Middle => {
2024-10-09 02:19:22 +00:00
let old = self.middle.replace(step.clone());
(old, step)
}
Position::Bottom => {
2024-10-09 02:19:22 +00:00
let old = self.middle.replace(step.clone());
(old, step)
}
2024-10-09 02:19:22 +00:00
};
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(),
}),
2024-10-08 03:47:17 +00:00
}
}
}
#[derive(Default)]
pub struct CyberScreenPrivate {
state: Rc<RefCell<CyberScreenState>>,
2024-10-09 02:19:22 +00:00
// 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]
impl ObjectSubclass for CyberScreenPrivate {
const NAME: &'static str = "CyberScreen";
type Type = CyberScreen;
type ParentType = gtk::DrawingArea;
}
impl ObjectImpl for CyberScreenPrivate {}
impl WidgetImpl for CyberScreenPrivate {}
impl DrawingAreaImpl for CyberScreenPrivate {}
impl CyberScreenPrivate {
fn set_script(&self, script: Script) {
*self.state.borrow_mut() = CyberScreenState::new(script);
}
fn next_page(&self) {
let transition = self.state.borrow_mut().next_page();
2024-10-09 02:19:22 +00:00
self.animations
.borrow_mut()
.insert(transition.position(), transition);
}
}
glib::wrapper! {
pub struct CyberScreen(ObjectSubclass<CyberScreenPrivate>) @extends gtk::DrawingArea, gtk::Widget;
}
impl CyberScreen {
pub fn new(script: Script) -> Self {
let s: Self = Object::builder().build();
s.imp().set_script(script);
s.set_draw_func({
let s = s.clone();
move |_, context, width, height| {
2024-10-08 03:47:17 +00:00
let now = Instant::now();
let _ = context.set_source_rgb(0., 0., 0.);
let _ = context.paint();
let pen = GlowPen::new(width, height, 2., 8., (0.7, 0., 1.));
/*
for i in 0..6 {
pen.move_to(0., height as f64 * i as f64 / 5.);
pen.line_to(width as f64, height as f64 * i as f64 / 5.);
}
pen.stroke();
*/
2024-10-09 02:19:22 +00:00
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();
2024-10-08 03:47:17 +00:00
let mut animations = s.imp().animations.borrow_mut();
2024-10-09 02:19:22 +00:00
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,
2024-10-08 03:47:17 +00:00
};
2024-10-09 02:19:22 +00:00
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,
2024-10-08 03:47:17 +00:00
};
2024-10-09 02:19:22 +00:00
animation.tick(now, context, region);
2024-10-08 03:47:17 +00:00
}
2024-10-09 02:19:22 +00:00
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);
}
}
});
s
}
fn next_page(&self) {
println!("next page");
self.imp().next_page();
self.queue_draw();
}
}
fn main() {
2024-10-08 03:47:17 +00:00
let script = Script(vec![Step {
text: "The distinguishing thing".to_owned(),
position: Position::Top,
transition: Duration::from_secs(2),
}]);
println!("{}", serde_yml::to_string(&script).unwrap());
let script = Script::from_file(Path::new("./script.yml")).unwrap();
for element in script.iter() {
println!("{:?}", element);
}
let app = gtk::Application::builder()
.application_id("com.luminescent-dreams.cyberpunk-slideshow")
.build();
app.connect_activate(move |app| {
let screen = CyberScreen::new(script.clone());
let events = EventControllerKey::new();
events.connect_key_released({
let screen = screen.clone();
move |_, key, _, _| {
if key.name() == Some(GString::from("Right")) {
screen.next_page();
}
}
});
let window = gtk::ApplicationWindow::new(app);
window.add_controller(events);
window.set_child(Some(&screen));
window.set_width_request(800);
window.set_height_request(600);
window.present();
2024-10-08 03:47:17 +00:00
let _ = glib::spawn_future_local({
let screen = screen.clone();
2024-10-09 02:19:22 +00:00
async move {
loop {
screen.queue_draw();
async_std::task::sleep(Duration::from_millis(1000 / FPS)).await;
}
}
2024-10-08 03:47:17 +00:00
});
});
app.run();
}