Compare commits
5 Commits
791f2be3c5
...
fc70bb3955
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | fc70bb3955 | |
Savanni D'Gerinel | 7b50a71369 | |
Savanni D'Gerinel | 7a7548c78f | |
Savanni D'Gerinel | 9c56e988b2 | |
Savanni D'Gerinel | de35ebb644 |
File diff suppressed because it is too large
Load Diff
|
@ -9,6 +9,8 @@ members = [
|
||||||
"config",
|
"config",
|
||||||
"config-derive",
|
"config-derive",
|
||||||
"coordinates",
|
"coordinates",
|
||||||
|
"cyberpunk",
|
||||||
|
"cyberpunk-slideshow",
|
||||||
"cyberpunk-splash",
|
"cyberpunk-splash",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"emseries",
|
"emseries",
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "cyberpunk-slideshow"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = "1.13.0"
|
||||||
|
cairo-rs = "0.18"
|
||||||
|
cyberpunk = { path = "../cyberpunk" }
|
||||||
|
gio = "0.18"
|
||||||
|
glib = "0.18"
|
||||||
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
|
serde_yml = "0.0.12"
|
|
@ -0,0 +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: 1
|
||||||
|
nanos: 0
|
||||||
|
|
||||||
|
- text: Any sufficiently advanced technology is indistinguishable from magic. -- Arthur C. Clark.
|
||||||
|
position: middle
|
||||||
|
transition:
|
||||||
|
secs: 1
|
||||||
|
nanos: 0
|
||||||
|
|
||||||
|
- text: Science is our Magic.
|
||||||
|
position: middle
|
||||||
|
transition:
|
||||||
|
secs: 1
|
||||||
|
nanos: 0
|
||||||
|
|
|
@ -0,0 +1,396 @@
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
io::Read,
|
||||||
|
ops::Index,
|
||||||
|
path::Path,
|
||||||
|
rc::Rc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use cairo::Context;
|
||||||
|
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text};
|
||||||
|
use glib::{GString, Object};
|
||||||
|
use gtk::{
|
||||||
|
glib::{self, Propagation},
|
||||||
|
prelude::*,
|
||||||
|
subclass::prelude::*,
|
||||||
|
EventControllerKey,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const FPS: u64 = 60;
|
||||||
|
const PURPLE: (f64, f64, f64) = (0.7, 0., 1.);
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Region {
|
||||||
|
left: f64,
|
||||||
|
top: f64,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fade {
|
||||||
|
text: String,
|
||||||
|
position: Position,
|
||||||
|
duration: Duration,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Animation {
|
||||||
|
fn position(&self) -> Position;
|
||||||
|
|
||||||
|
fn tick(&self, now: Instant, context: &Context, region: Region);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Fade {
|
||||||
|
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.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();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_page(&mut self) -> Box<dyn Animation> {
|
||||||
|
let idx = match self.idx {
|
||||||
|
None => 0,
|
||||||
|
Some(idx) => {
|
||||||
|
if idx < self.script.len() {
|
||||||
|
idx + 1
|
||||||
|
} else {
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.idx = Some(idx);
|
||||||
|
let step = self.script[idx].clone();
|
||||||
|
|
||||||
|
let (old, new) = match step.position {
|
||||||
|
Position::Top => {
|
||||||
|
let old = self.top.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
Position::Middle => {
|
||||||
|
let old = self.middle.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
Position::Bottom => {
|
||||||
|
let old = self.middle.replace(step.clone());
|
||||||
|
(old, step)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CyberScreenPrivate {
|
||||||
|
state: Rc<RefCell<CyberScreenState>>,
|
||||||
|
// 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();
|
||||||
|
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| {
|
||||||
|
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();
|
||||||
|
*/
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_page(&self) {
|
||||||
|
println!("next page");
|
||||||
|
self.imp().next_page();
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
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();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.run();
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cairo-rs = { version = "0.18" }
|
cairo-rs = { version = "0.18" }
|
||||||
|
cyberpunk = { path = "../cyberpunk" }
|
||||||
gio = { version = "0.18" }
|
gio = { version = "0.18" }
|
||||||
glib = { version = "0.18" }
|
glib = { version = "0.18" }
|
||||||
gtk = { version = "0.7", package = "gtk4" }
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
|
|
|
@ -419,212 +419,6 @@ impl Splash {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|
||||||
fn draw(&self, pen: &impl Pen) {
|
|
||||||
let dodge = if self.invert {
|
|
||||||
self.height
|
|
||||||
} else {
|
|
||||||
-self.height
|
|
||||||
};
|
|
||||||
match self.orientation {
|
|
||||||
gtk::Orientation::Horizontal => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height + self.cutout_length,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x
|
|
||||||
+ self.start_length
|
|
||||||
+ self.height
|
|
||||||
+ self.cutout_length
|
|
||||||
+ (self.height / 2.),
|
|
||||||
self.start_y + dodge / 2.,
|
|
||||||
);
|
|
||||||
pen.line_to(self.total_length, self.start_y + dodge / 2.);
|
|
||||||
}
|
|
||||||
gtk::Orientation::Vertical => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x, self.start_y + self.start_length);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge,
|
|
||||||
self.start_y + self.start_length + self.height,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge,
|
|
||||||
self.start_y + self.start_length + self.height + self.cutout_length,
|
|
||||||
);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + dodge / 2.,
|
|
||||||
self.start_y
|
|
||||||
+ self.start_length
|
|
||||||
+ self.height
|
|
||||||
+ self.cutout_length
|
|
||||||
+ (self.height / 2.),
|
|
||||||
);
|
|
||||||
pen.line_to(self.start_x + dodge / 2., self.total_length);
|
|
||||||
}
|
|
||||||
_ => panic!("unknown orientation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AsymLine {
|
|
||||||
orientation: gtk::Orientation,
|
|
||||||
start_x: f64,
|
|
||||||
start_y: f64,
|
|
||||||
start_length: f64,
|
|
||||||
height: f64,
|
|
||||||
total_length: f64,
|
|
||||||
invert: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsymLine {
|
|
||||||
fn draw(&self, pen: &impl Pen) {
|
|
||||||
let dodge = if self.invert {
|
|
||||||
self.height
|
|
||||||
} else {
|
|
||||||
-self.height
|
|
||||||
};
|
|
||||||
match self.orientation {
|
|
||||||
gtk::Orientation::Horizontal => {
|
|
||||||
pen.move_to(self.start_x, self.start_y);
|
|
||||||
pen.line_to(self.start_x + self.start_length, self.start_y);
|
|
||||||
pen.line_to(
|
|
||||||
self.start_x + self.start_length + self.height,
|
|
||||||
self.start_y + dodge,
|
|
||||||
);
|
|
||||||
pen.line_to(self.start_x + self.total_length, self.start_y + dodge);
|
|
||||||
}
|
|
||||||
gtk::Orientation::Vertical => {}
|
|
||||||
_ => panic!("unknown orientation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
let width = self.length / (self.count as f64 * 2.);
|
|
||||||
|
|
||||||
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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
self.blur_context.stroke().expect("to draw the blur line");
|
|
||||||
self.draw_context
|
|
||||||
.stroke()
|
|
||||||
.expect("to draw the regular line");
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
fn main() {
|
||||||
let app = gtk::Application::builder()
|
let app = gtk::Application::builder()
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "cyberpunk"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cairo-rs = { version = "0.18" }
|
||||||
|
gio = { version = "0.18" }
|
||||||
|
glib = { version = "0.18" }
|
||||||
|
gtk = { version = "0.7", package = "gtk4" }
|
|
@ -0,0 +1,272 @@
|
||||||
|
use cairo::{
|
||||||
|
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, Pattern,
|
||||||
|
TextExtents,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AsymLineCutout {
|
||||||
|
pub orientation: gtk::Orientation,
|
||||||
|
pub start_x: f64,
|
||||||
|
pub start_y: f64,
|
||||||
|
pub start_length: f64,
|
||||||
|
pub cutout_length: f64,
|
||||||
|
pub end_length: f64,
|
||||||
|
pub height: f64,
|
||||||
|
pub invert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsymLineCutout {
|
||||||
|
pub fn draw(&self, pen: &impl Pen) {
|
||||||
|
let dodge = if self.invert {
|
||||||
|
self.height
|
||||||
|
} else {
|
||||||
|
-self.height
|
||||||
|
};
|
||||||
|
match self.orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height + self.cutout_length,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.),
|
||||||
|
self.start_y + dodge / 2.,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.)
|
||||||
|
+ self.end_length,
|
||||||
|
self.start_y + dodge / 2.,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x, self.start_y + self.start_length);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge,
|
||||||
|
self.start_y + self.start_length + self.height,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge,
|
||||||
|
self.start_y + self.start_length + self.height + self.cutout_length,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge / 2.,
|
||||||
|
self.start_y
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.),
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + dodge / 2.,
|
||||||
|
self.start_y
|
||||||
|
+ self.start_length
|
||||||
|
+ self.height
|
||||||
|
+ self.cutout_length
|
||||||
|
+ (self.height / 2.)
|
||||||
|
+ self.end_length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("unknown orientation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents an asymetrical line that starts at one location, then a 45-degree angle and then
|
||||||
|
// another line afterwards.
|
||||||
|
pub struct AsymLine {
|
||||||
|
// Will this be drawn left-to-right or up-to-down?
|
||||||
|
pub orientation: gtk::Orientation,
|
||||||
|
|
||||||
|
// Starting address
|
||||||
|
pub start_x: f64,
|
||||||
|
pub start_y: f64,
|
||||||
|
|
||||||
|
// Length of the first segment
|
||||||
|
pub start_length: f64,
|
||||||
|
|
||||||
|
// Height to dodge over to the next section
|
||||||
|
pub height: f64,
|
||||||
|
|
||||||
|
// Total length of the entire line.
|
||||||
|
pub end_length: f64,
|
||||||
|
|
||||||
|
// When normal, the angle dodge is upwards. When inverted, the angle dodge is downwards.
|
||||||
|
pub invert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsymLine {
|
||||||
|
pub fn draw(&self, pen: &impl Pen) {
|
||||||
|
let dodge = if self.invert {
|
||||||
|
self.height
|
||||||
|
} else {
|
||||||
|
-self.height
|
||||||
|
};
|
||||||
|
match self.orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
pen.move_to(self.start_x, self.start_y);
|
||||||
|
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
pen.line_to(
|
||||||
|
self.start_x + self.start_length + self.height + self.end_length,
|
||||||
|
self.start_y + dodge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => {}
|
||||||
|
_ => panic!("unknown orientation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
let width = self.length / (self.count as f64 * 2.);
|
||||||
|
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a pen for drawing a pattern. This is good for complex patterns that may require
|
||||||
|
/// multiple identical steps.
|
||||||
|
pub trait Pen {
|
||||||
|
/// Move the pen to a location.
|
||||||
|
fn move_to(&self, x: f64, y: f64);
|
||||||
|
|
||||||
|
/// Draw a line from the current location to the specified destination.
|
||||||
|
fn line_to(&self, x: f64, y: f64);
|
||||||
|
|
||||||
|
/// Instantiate the line.
|
||||||
|
fn stroke(&self);
|
||||||
|
|
||||||
|
/// Convert all of the drawing into a pattern that can be painted to a drawing context.
|
||||||
|
fn finish(self) -> Pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GlowPen {
|
||||||
|
blur_context: Context,
|
||||||
|
draw_context: Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlowPen {
|
||||||
|
pub 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) {
|
||||||
|
self.blur_context.stroke().expect("to draw the blur line");
|
||||||
|
self.draw_context
|
||||||
|
.stroke()
|
||||||
|
.expect("to draw the regular line");
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Text<'a> {
|
||||||
|
content: String,
|
||||||
|
context: &'a Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Text<'a> {
|
||||||
|
pub fn new(content: String, context: &'a Context, size: f64) -> Self {
|
||||||
|
context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);
|
||||||
|
context.set_font_size(size);
|
||||||
|
Self { content, context }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extents(&self) -> TextExtents {
|
||||||
|
self.context.text_extents(&self.content).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&self) {
|
||||||
|
let _ = self.context.show_text(&self.content);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue