monorepo/cyberpunk-splash/src/main.rs

757 lines
24 KiB
Rust
Raw Normal View History

2023-04-09 22:20:29 +00:00
use cairo::{
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
2023-04-10 01:07:12 +00:00
TextExtents,
2023-04-09 22:20:29 +00:00
};
2023-10-04 20:20:28 +00:00
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
2023-04-10 00:31:52 +00:00
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, RwLock},
2023-04-10 00:31:52 +00:00
time::{Duration, Instant},
};
const WIDTH: i32 = 1600;
const HEIGHT: i32 = 600;
#[derive(Clone, Copy, Debug)]
pub enum State {
Running {
last_update: Instant,
deadline: Instant,
timeout: Option<TimeoutAnimation>,
},
Paused {
time_remaining: Duration,
timeout: Option<TimeoutAnimation>,
},
}
impl State {
fn new(countdown: Duration) -> Self {
Self::Paused {
time_remaining: countdown,
timeout: None,
}
}
fn start(&mut self) {
if let Self::Paused {
time_remaining,
timeout,
} = self
{
*self = Self::Running {
last_update: Instant::now(),
deadline: Instant::now() + *time_remaining,
2023-10-04 20:20:28 +00:00
timeout: *timeout,
};
}
}
fn pause(&mut self) {
if let Self::Running {
deadline, timeout, ..
} = self
{
*self = Self::Paused {
time_remaining: *deadline - Instant::now(),
2023-10-04 20:20:28 +00:00
timeout: *timeout,
}
}
}
fn start_pause(&mut self) {
match self {
Self::Running { .. } => self.pause(),
Self::Paused { .. } => self.start(),
}
}
fn run(&mut self, now: Instant) {
if let Self::Running {
last_update,
deadline,
timeout,
} = self
{
*last_update = now;
if let Some(ref mut timeout) = timeout {
// TODO: figure out the actual number of frames
timeout.tick(1);
}
if *last_update > *deadline && timeout.is_none() {
*timeout = Some(TimeoutAnimation {
intensity: 1.,
duration: 1.,
ascending: false,
});
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TimeoutAnimation {
intensity: f64,
duration: f64,
ascending: bool,
}
impl TimeoutAnimation {
fn tick(&mut self, frames_elapsed: u8) {
let step_size = 1. / (self.duration * 60.);
if self.ascending {
2023-10-04 20:20:28 +00:00
self.intensity += step_size * frames_elapsed as f64;
if self.intensity > 1. {
self.intensity = 1.0;
self.ascending = false;
}
} else {
2023-10-04 20:20:28 +00:00
self.intensity -= step_size * frames_elapsed as f64;
if self.intensity < 0. {
self.intensity = 0.0;
self.ascending = true;
}
}
}
}
pub struct SplashPrivate {
text: Rc<RefCell<String>>,
background: Rc<RefCell<Pattern>>,
2023-04-10 01:07:12 +00:00
time_extents: Rc<RefCell<Option<TextExtents>>>,
width: Rc<RefCell<i32>>,
height: Rc<RefCell<i32>>,
state: Rc<RefCell<State>>,
}
impl SplashPrivate {
fn set_text(&self, text: String) {
*self.text.borrow_mut() = text;
}
fn set_state(&self, state: State) {
*self.state.borrow_mut() = state;
}
fn redraw_background(&self) {
2023-04-18 13:33:45 +00:00
let pen = GlowPen::new(
*self.width.borrow(),
*self.height.borrow(),
2.,
8.,
(0.7, 0., 1.),
);
let background =
ImageSurface::create(Format::Rgb24, *self.width.borrow(), *self.height.borrow())
.unwrap();
let context = Context::new(background).unwrap();
context.push_group();
context.set_source_rgb(0., 0., 0.);
let _ = context.paint();
2023-04-09 22:20:29 +00:00
context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);
2023-04-18 13:56:35 +00:00
{
context.set_source_rgb(0.7, 0., 1.);
2023-04-09 22:20:29 +00:00
2023-04-18 13:56:35 +00:00
let hashtag = "#CodingTogether";
context.set_font_size(64.);
let extents = context.text_extents(hashtag).unwrap();
2023-04-09 22:20:29 +00:00
2023-04-18 13:56:35 +00:00
context.move_to(20., extents.height() + 40.);
let _ = context.show_text(hashtag);
2023-04-10 00:07:12 +00:00
2023-04-18 13:56:35 +00:00
AsymLine {
2023-04-10 00:07:12 +00:00
orientation: gtk::Orientation::Horizontal,
2023-04-18 13:56:35 +00:00
start_x: 10.,
start_y: extents.height() + 10.,
start_length: 0.,
height: extents.height() / 2.,
total_length: extents.width() + extents.height() / 2.,
2023-04-10 00:07:12 +00:00
invert: false,
2023-04-18 13:56:35 +00:00
}
.draw(&pen);
pen.stroke();
2023-04-10 00:07:12 +00:00
2023-04-18 13:56:35 +00:00
AsymLine {
orientation: gtk::Orientation::Horizontal,
start_x: 20.,
start_y: extents.height() + 60.,
start_length: extents.width(),
height: extents.height() / 2.,
total_length: extents.width() + extents.height() / 2.,
invert: false,
}
.draw(&pen);
2023-04-18 13:33:45 +00:00
pen.stroke();
2023-04-10 00:07:12 +00:00
}
{
2023-04-18 13:56:35 +00:00
context.set_font_size(128.);
let center_x = *self.width.borrow() as f64 / 2.;
let center_y = *self.height.borrow() as f64 / 2.;
let title_extents = context.text_extents(&self.text.borrow()).unwrap();
let title_width = title_extents.width();
let title_height = title_extents.height();
{
let start_length = center_x - title_width / 2. - title_height - 20.;
let title_cutout = AsymLineCutout {
orientation: gtk::Orientation::Horizontal,
start_x: 20.,
start_y: center_y - 20. - title_height / 2.,
start_length,
total_length: *self.width.borrow() as f64 - 120.,
cutout_length: title_width,
height: title_height,
invert: false,
};
title_cutout.draw(&pen);
pen.stroke();
}
{
let title_baseline_x = center_x - title_width / 2.;
let title_baseline_y = center_y - 20.;
let gradient = LinearGradient::new(
title_baseline_x,
title_baseline_y - title_height,
title_baseline_x,
title_baseline_y,
);
gradient.add_color_stop_rgb(0.2, 0.7, 0.0, 1.0);
gradient.add_color_stop_rgb(0.8, 0.2, 0.0, 1.0);
context.move_to(title_baseline_x, title_baseline_y);
let _ = context.set_source(gradient);
let _ = context.show_text(&self.text.borrow());
}
2023-04-10 00:07:12 +00:00
}
2023-04-09 22:20:29 +00:00
{
2023-04-10 00:07:12 +00:00
AsymLine {
orientation: gtk::Orientation::Horizontal,
start_x: 100.,
start_y: *self.height.borrow() as f64 / 2. + 100.,
2023-04-10 00:07:12 +00:00
start_length: 400.,
height: 50.,
total_length: 650.,
invert: true,
}
2023-04-18 13:33:45 +00:00
.draw(&pen);
pen.stroke();
2023-04-10 00:07:12 +00:00
}
2023-04-09 22:20:29 +00:00
2023-04-10 00:07:12 +00:00
{
context.set_source_rgb(0.7, 0., 1.);
AsymLine {
2023-04-09 22:20:29 +00:00
orientation: gtk::Orientation::Horizontal,
start_x: *self.width.borrow() as f64 / 2. + 100.,
start_y: *self.height.borrow() as f64 / 2. + 200.,
2023-04-10 00:07:12 +00:00
start_length: 600.,
height: 50.,
total_length: 650.,
2023-04-09 22:20:29 +00:00
invert: false,
}
2023-04-18 13:33:45 +00:00
.draw(&pen);
pen.stroke();
}
2023-04-18 13:33:45 +00:00
let tracery = pen.finish();
let _ = context.set_source(tracery);
let _ = context.paint();
let background = context.pop_group().unwrap();
*self.background.borrow_mut() = background;
}
}
#[glib::object_subclass]
impl ObjectSubclass for SplashPrivate {
const NAME: &'static str = "Splash";
type Type = Splash;
type ParentType = gtk::DrawingArea;
fn new() -> SplashPrivate {
// Set up a default plain black background
let background = ImageSurface::create(Format::Rgb24, WIDTH, HEIGHT).unwrap();
let context = Context::new(background).unwrap();
context.push_group();
context.set_source_rgb(0., 0., 0.);
let _ = context.paint();
let background = context.pop_group().unwrap();
SplashPrivate {
text: Rc::new(RefCell::new(String::from(""))),
background: Rc::new(RefCell::new(background)),
2023-04-10 01:07:12 +00:00
time_extents: Rc::new(RefCell::new(None)),
width: Rc::new(RefCell::new(WIDTH)),
height: Rc::new(RefCell::new(HEIGHT)),
state: Rc::new(RefCell::new(State::new(Duration::ZERO))),
}
}
}
impl ObjectImpl for SplashPrivate {}
impl WidgetImpl for SplashPrivate {}
impl DrawingAreaImpl for SplashPrivate {}
glib::wrapper! {
pub struct Splash(ObjectSubclass<SplashPrivate>) @extends gtk::DrawingArea, gtk::Widget;
}
impl Splash {
pub fn new(text: String, state: State) -> Self {
let s: Self = Object::builder().build();
s.set_width_request(WIDTH);
s.set_height_request(HEIGHT);
s.imp().set_text(text);
s.imp().set_state(state);
s.imp().redraw_background();
s.set_draw_func({
let s = s.clone();
2023-04-10 00:31:52 +00:00
move |_, context, width, height| {
let background = s.imp().background.borrow();
let _ = context.set_source(&*background);
let _ = context.paint();
2023-04-10 00:31:52 +00:00
2023-10-04 20:20:28 +00:00
let state = *s.imp().state.borrow();
let time = match state {
State::Running { deadline, .. } => deadline - Instant::now(),
State::Paused { time_remaining, .. } => time_remaining,
};
2023-04-10 01:07:12 +00:00
let minutes = time.as_secs() / 60;
let seconds = time.as_secs() % 60;
let center_x = width as f64 / 2.;
let center_y = height as f64 / 2.;
2023-04-10 00:31:52 +00:00
2023-04-10 01:07:12 +00:00
{
2023-04-10 00:31:52 +00:00
context.select_font_face(
"Alegreya Sans SC",
FontSlant::Normal,
FontWeight::Bold,
);
context.set_font_size(128.);
let time = format!("{:02}' {:02}\"", minutes, seconds);
let time_extents = context.text_extents(&time).unwrap();
2023-04-10 01:07:12 +00:00
let mut saved_extents = s.imp().time_extents.borrow_mut();
if saved_extents.is_none() {
2023-10-04 20:20:28 +00:00
*saved_extents = Some(time_extents);
2023-04-10 01:07:12 +00:00
}
2023-04-10 00:31:52 +00:00
let time_baseline_x = center_x - time_extents.width() / 2.;
let time_baseline_y = center_y + 100.;
let gradient = LinearGradient::new(
time_baseline_x,
time_baseline_y - time_extents.height(),
time_baseline_x,
time_baseline_y,
);
let (running, timeout_animation) = match state {
2023-10-04 20:20:28 +00:00
State::Running { timeout, .. } => (true, timeout),
State::Paused { timeout, .. } => (false, timeout),
};
match timeout_animation {
Some(ref animation) => {
gradient.add_color_stop_rgba(0.2, 0.2, 0.0, 1.0, animation.intensity);
gradient.add_color_stop_rgba(0.8, 0.7, 0.0, 1.0, animation.intensity);
let _ = context.set_source(gradient);
}
None => {
if running {
gradient.add_color_stop_rgb(0.2, 0.2, 0.0, 1.0);
gradient.add_color_stop_rgb(0.8, 0.7, 0.0, 1.0);
let _ = context.set_source(gradient);
} else {
context.set_source_rgb(0.3, 0.3, 0.3);
}
}
}
2023-04-10 00:31:52 +00:00
context.move_to(time_baseline_x, time_baseline_y);
let _ = context.show_text(&time);
2023-04-10 01:07:12 +00:00
};
2023-10-04 20:20:28 +00:00
if let Some(extents) = *s.imp().time_extents.borrow() {
context.set_source_rgb(0.7, 0.0, 1.0);
let time_meter = SlashMeter {
orientation: gtk::Orientation::Horizontal,
start_x: center_x + extents.width() / 2. + 50.,
start_y: center_y + 100.,
count: 5,
fill_count: minutes as u8,
height: 60.,
length: 100.,
};
time_meter.draw(context);
2023-04-10 00:31:52 +00:00
}
}
});
s.connect_resize(|s, width, height| {
*s.imp().width.borrow_mut() = width;
*s.imp().height.borrow_mut() = height;
s.imp().redraw_background();
});
s
}
2023-04-10 00:31:52 +00:00
pub fn set_state(&self, state: State) {
self.imp().set_state(state);
2023-04-10 00:31:52 +00:00
self.queue_draw();
}
}
2023-04-09 22:20:29 +00:00
struct AsymLineCutout {
orientation: gtk::Orientation,
start_x: f64,
start_y: f64,
start_length: f64,
total_length: f64,
cutout_length: f64,
height: f64,
invert: bool,
}
impl AsymLineCutout {
2023-04-18 13:33:45 +00:00
fn draw(&self, pen: &impl Pen) {
2023-04-09 22:20:29 +00:00
let dodge = if self.invert {
self.height
} else {
-self.height
};
match self.orientation {
gtk::Orientation::Horizontal => {
2023-04-18 13:33:45 +00:00
pen.move_to(self.start_x, self.start_y);
pen.line_to(self.start_x + self.start_length, self.start_y);
pen.line_to(
2023-04-09 22:20:29 +00:00
self.start_x + self.start_length + self.height,
self.start_y + dodge,
);
2023-04-18 13:33:45 +00:00
pen.line_to(
2023-04-09 22:20:29 +00:00
self.start_x + self.start_length + self.height + self.cutout_length,
self.start_y + dodge,
);
2023-04-18 13:33:45 +00:00
pen.line_to(
2023-04-09 22:20:29 +00:00
self.start_x
+ self.start_length
+ self.height
+ self.cutout_length
+ (self.height / 2.),
self.start_y + dodge / 2.,
);
2023-04-18 13:33:45 +00:00
pen.line_to(self.total_length, self.start_y + dodge / 2.);
2023-04-09 22:20:29 +00:00
}
gtk::Orientation::Vertical => {
2023-04-18 13:33:45 +00:00
pen.move_to(self.start_x, self.start_y);
pen.line_to(self.start_x, self.start_y + self.start_length);
pen.line_to(
2023-04-10 00:07:12 +00:00
self.start_x + dodge,
2023-04-09 22:20:29 +00:00
self.start_y + self.start_length + self.height,
);
2023-04-18 13:33:45 +00:00
pen.line_to(
2023-04-10 00:07:12 +00:00
self.start_x + dodge,
self.start_y + self.start_length + self.height + self.cutout_length,
2023-04-09 22:20:29 +00:00
);
2023-04-18 13:33:45 +00:00
pen.line_to(
2023-04-10 00:07:12 +00:00
self.start_x + dodge / 2.,
2023-04-09 22:20:29 +00:00
self.start_y
+ self.start_length
+ self.height
2023-04-10 00:07:12 +00:00
+ self.cutout_length
2023-04-09 22:20:29 +00:00
+ (self.height / 2.),
);
2023-04-18 13:33:45 +00:00
pen.line_to(self.start_x + dodge / 2., self.total_length);
2023-04-09 22:20:29 +00:00
}
_ => panic!("unknown orientation"),
}
}
}
2023-04-10 00:07:12 +00:00
struct AsymLine {
orientation: gtk::Orientation,
start_x: f64,
start_y: f64,
start_length: f64,
2023-04-10 00:07:12 +00:00
height: f64,
total_length: f64,
invert: bool,
}
2023-04-10 00:07:12 +00:00
impl AsymLine {
2023-04-18 13:33:45 +00:00
fn draw(&self, pen: &impl Pen) {
2023-04-10 00:07:12 +00:00
let dodge = if self.invert {
self.height
} else {
-self.height
};
match self.orientation {
gtk::Orientation::Horizontal => {
2023-04-18 13:33:45 +00:00
pen.move_to(self.start_x, self.start_y);
pen.line_to(self.start_x + self.start_length, self.start_y);
pen.line_to(
2023-04-10 00:07:12 +00:00
self.start_x + self.start_length + self.height,
self.start_y + dodge,
);
2023-04-18 13:33:45 +00:00
pen.line_to(self.start_x + self.total_length, self.start_y + dodge);
}
2023-04-10 00:07:12 +00:00
gtk::Orientation::Vertical => {}
_ => panic!("unknown orientation"),
}
}
}
2023-04-10 01:07:12 +00:00
struct SlashMeter {
orientation: gtk::Orientation,
start_x: f64,
start_y: f64,
count: u8,
fill_count: u8,
height: f64,
length: f64,
}
impl SlashMeter {
fn draw(&self, context: &Context) {
match self.orientation {
gtk::Orientation::Horizontal => {
let angle: f64 = 0.8;
let run = self.height / angle.tan();
2023-10-04 20:20:28 +00:00
let width = self.length / (self.count as f64 * 2.);
2023-04-10 01:07:12 +00:00
for c in 0..self.count {
context.set_line_width(1.);
let start_x = self.start_x + c as f64 * width * 2.;
context.move_to(start_x, self.start_y);
context.line_to(start_x + run, self.start_y - self.height);
context.line_to(start_x + run + width, self.start_y - self.height);
context.line_to(start_x + width, self.start_y);
context.line_to(start_x, self.start_y);
if c < self.fill_count {
let _ = context.fill();
} else {
let _ = context.stroke();
}
}
}
gtk::Orientation::Vertical => {}
_ => panic!("unknown orientation"),
}
}
}
2023-04-18 13:33:45 +00:00
trait Pen {
fn move_to(&self, x: f64, y: f64);
fn line_to(&self, x: f64, y: f64);
fn stroke(&self);
fn finish(self) -> Pattern;
}
struct GlowPen {
blur_context: Context,
draw_context: Context,
}
impl GlowPen {
fn new(
width: i32,
height: i32,
line_width: f64,
blur_line_width: f64,
color: (f64, f64, f64),
) -> Self {
let blur_context =
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
blur_context.set_line_width(blur_line_width);
blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
blur_context.push_group();
blur_context.set_line_cap(LineCap::Round);
let draw_context =
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
draw_context.set_line_width(line_width);
draw_context.set_source_rgb(color.0, color.1, color.2);
draw_context.push_group();
draw_context.set_line_cap(LineCap::Round);
Self {
blur_context,
draw_context,
}
}
}
impl Pen for GlowPen {
fn move_to(&self, x: f64, y: f64) {
self.blur_context.move_to(x, y);
self.draw_context.move_to(x, y);
}
fn line_to(&self, x: f64, y: f64) {
self.blur_context.line_to(x, y);
self.draw_context.line_to(x, y);
}
fn stroke(&self) {
2023-10-04 20:20:28 +00:00
self.blur_context.stroke().expect("to draw the blur line");
self.draw_context
.stroke()
.expect("to draw the regular line");
2023-04-18 13:33:45 +00:00
}
fn finish(self) -> Pattern {
let foreground = self.draw_context.pop_group().unwrap();
self.blur_context.set_source(foreground).unwrap();
self.blur_context.paint().unwrap();
self.blur_context.pop_group().unwrap()
}
}
fn main() {
let app = gtk::Application::builder()
.application_id("com.luminescent-dreams.cyberpunk-splash")
2023-04-10 14:46:35 +00:00
.flags(gio::ApplicationFlags::HANDLES_OPEN)
.build();
2023-04-10 14:46:35 +00:00
app.add_main_option(
"title",
glib::char::Char::from(b't'),
glib::OptionFlags::IN_MAIN,
glib::OptionArg::String,
"",
None,
);
app.add_main_option(
"countdown",
glib::char::Char::from(b'c'),
glib::OptionFlags::IN_MAIN,
glib::OptionArg::String,
"",
None,
);
let state = Arc::new(RwLock::new(State::new(Duration::from_secs(5 * 60))));
let title = Arc::new(RwLock::new("".to_owned()));
2023-04-13 22:56:42 +00:00
app.connect_command_line(|_, _args| {
2023-04-10 14:46:35 +00:00
println!("connect_command_line");
1
});
app.connect_handle_local_options({
let title = title.clone();
2023-04-13 22:56:42 +00:00
let state = state.clone();
2023-04-10 14:46:35 +00:00
move |_, options| {
println!("connect_handle_local_options");
*title.write().unwrap() = options.lookup::<String>("title").unwrap().unwrap();
2023-04-13 22:56:42 +00:00
let countdown = match options.lookup::<String>("countdown") {
Ok(Some(countdown_str)) => {
let parts = countdown_str.split(':').collect::<Vec<&str>>();
2023-10-04 20:20:28 +00:00
match parts.len() {
2023-04-13 22:56:42 +00:00
2 => {
let minutes = parts[0].parse::<u64>().unwrap();
let seconds = parts[1].parse::<u64>().unwrap();
Duration::from_secs(minutes * 60 + seconds)
}
1 => {
let seconds = parts[1].parse::<u64>().unwrap();
Duration::from_secs(seconds)
}
_ => Duration::from_secs(300),
2023-10-04 20:20:28 +00:00
}
2023-04-13 22:56:42 +00:00
}
_ => Duration::from_secs(300),
};
match *state.write().unwrap() {
State::Running {
ref mut deadline, ..
} => *deadline = Instant::now() + countdown,
State::Paused {
ref mut time_remaining,
..
} => *time_remaining = countdown,
}
2023-04-10 14:46:35 +00:00
-1
}
});
app.connect_open(move |app, files, args| {
println!("called open");
println!("files: {}", files.len());
println!("args: {}", args);
app.activate();
});
app.connect_activate(move |app| {
2023-04-10 00:31:52 +00:00
let (gtk_tx, gtk_rx) =
gtk::glib::MainContext::channel::<State>(gtk::glib::Priority::DEFAULT);
2023-04-10 14:46:35 +00:00
let window = gtk::ApplicationWindow::new(app);
window.present();
2023-10-04 20:20:28 +00:00
let splash = Splash::new(title.read().unwrap().clone(), *state.read().unwrap());
2023-04-10 00:31:52 +00:00
window.set_child(Some(&splash));
window.connect_maximized_notify(|window| {
window.fullscreen();
});
2023-04-10 00:31:52 +00:00
2023-04-10 04:20:39 +00:00
let keyboard_events = EventControllerKey::new();
keyboard_events.connect_key_released({
let window = window.clone();
let state = state.clone();
2023-04-10 04:20:39 +00:00
move |_, key, _, _| {
let name = key
.name()
.map(|s| s.as_str().to_owned())
.unwrap_or("".to_owned());
match name.as_ref() {
"Escape" => window.unfullscreen(),
"space" => state.write().unwrap().start_pause(),
2023-04-10 04:20:39 +00:00
_ => {}
}
}
});
window.add_controller(keyboard_events);
gtk_rx.attach(None, move |state| {
splash.set_state(state);
glib::ControlFlow::Continue
2023-04-10 00:31:52 +00:00
});
2023-04-10 14:46:35 +00:00
std::thread::spawn({
let state = state.clone();
move || {
state.write().unwrap().start();
loop {
std::thread::sleep(Duration::from_millis(1000 / 60));
state.write().unwrap().run(Instant::now());
2023-10-04 20:20:28 +00:00
let _ = gtk_tx.send(*state.read().unwrap());
2023-04-10 14:46:35 +00:00
}
2023-04-10 00:31:52 +00:00
}
});
});
app.run();
}