2024-05-25 11:53:15 +00:00
|
|
|
use crate::{iotro::Language, Resolution};
|
|
|
|
use fontconfig::Fontconfig;
|
|
|
|
use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
|
|
|
|
use std::sync::OnceLock;
|
|
|
|
use svgwriter::{
|
|
|
|
tags::{Group, Path, Rect, TSpan, TagWithPresentationAttributes as _, Text},
|
|
|
|
Data, Graphic, Transform
|
|
|
|
};
|
|
|
|
|
|
|
|
pub(crate) struct Question {
|
|
|
|
res: Resolution,
|
|
|
|
g: Group
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Question {
|
|
|
|
pub(crate) fn new(res: Resolution, lang: &Language<'_>, str: String) -> Self {
|
|
|
|
static FONT: OnceLock<Owned<Font<'static>>> = OnceLock::new();
|
|
|
|
let font = FONT.get_or_init(|| {
|
|
|
|
let fc = Fontconfig::new().unwrap();
|
|
|
|
let font_path = fc.find("Noto Sans", None).unwrap().path;
|
|
|
|
let face = Face::from_file(font_path, 0).unwrap();
|
|
|
|
Font::new(face)
|
|
|
|
});
|
|
|
|
let upem = font.face().upem();
|
|
|
|
|
|
|
|
// constants
|
|
|
|
let border_r = 12;
|
|
|
|
let font_size = 44;
|
|
|
|
let line_height = font_size * 6 / 5;
|
|
|
|
let padding = font_size / 2;
|
|
|
|
let margin_x = 240;
|
2024-05-25 12:17:07 +00:00
|
|
|
let margin_y = padding * 3 / 2;
|
2024-05-25 11:53:15 +00:00
|
|
|
let question_offset = 64;
|
|
|
|
let question_width = 240;
|
|
|
|
|
|
|
|
// calculated
|
|
|
|
let box_width = 1920 - 2 * margin_x;
|
|
|
|
let text_width = box_width - 2 * padding;
|
|
|
|
|
|
|
|
// calculates the width of the given string
|
|
|
|
let width_of = |s: &str| {
|
|
|
|
let width: i32 =
|
|
|
|
harfbuzz_rs::shape(font, UnicodeBuffer::new().add_str(s), &[])
|
|
|
|
.get_glyph_positions()
|
|
|
|
.iter()
|
|
|
|
.map(|glyph_pos| glyph_pos.x_advance)
|
|
|
|
.sum();
|
|
|
|
(width * font_size) / upem as i32
|
|
|
|
};
|
|
|
|
let space_width = width_of(" ");
|
|
|
|
|
|
|
|
// lay out the text
|
|
|
|
let mut text = Text::new()
|
|
|
|
.with_dominant_baseline("hanging")
|
|
|
|
.with_transform(
|
|
|
|
Transform::new().translate(padding, padding + font_size / 2 + border_r)
|
|
|
|
);
|
|
|
|
let words = str.split_whitespace();
|
|
|
|
let mut text_height = 0;
|
|
|
|
let mut text_x = 0;
|
|
|
|
for word in words {
|
|
|
|
let word_width = width_of(word);
|
|
|
|
if text_x + word_width > text_width {
|
|
|
|
text_x = 0;
|
|
|
|
text_height += line_height;
|
|
|
|
}
|
|
|
|
text.push(
|
|
|
|
TSpan::new()
|
|
|
|
.with_x(text_x)
|
|
|
|
.with_y(text_height)
|
|
|
|
.append(word.to_owned())
|
|
|
|
);
|
|
|
|
text_x += word_width + space_width;
|
|
|
|
}
|
|
|
|
text_height += font_size;
|
|
|
|
|
|
|
|
// calculated
|
|
|
|
let box_height = text_height + 2 * padding + font_size / 2 + border_r;
|
|
|
|
|
|
|
|
let mut g = Group::new()
|
|
|
|
.with_fill("white")
|
|
|
|
.with_font_family("Noto Sans")
|
|
|
|
.with_font_size(font_size)
|
|
|
|
.with_transform(
|
|
|
|
Transform::new().translate(margin_x, 1080 - margin_y - box_height)
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut outline = Data::new();
|
|
|
|
outline.move_by(border_r, 0).horiz_line_to(question_offset);
|
|
|
|
outline
|
|
|
|
.vert_line_by(-font_size / 2)
|
|
|
|
.arc_by(border_r, border_r, 0, false, true, border_r, -border_r)
|
|
|
|
.horiz_line_by(question_width)
|
|
|
|
.arc_by(border_r, border_r, 0, false, true, border_r, border_r)
|
|
|
|
.vert_line_by(font_size)
|
|
|
|
.arc_by(border_r, border_r, 0, false, true, -border_r, border_r)
|
|
|
|
.horiz_line_by(-question_width)
|
|
|
|
.arc_by(border_r, border_r, 0, false, true, -border_r, -border_r)
|
|
|
|
.vert_line_by(-font_size / 2)
|
|
|
|
.move_by(question_width + 2 * border_r, 0);
|
|
|
|
outline
|
|
|
|
.horiz_line_to(box_width - border_r)
|
|
|
|
.arc_by(border_r, border_r, 0, false, true, border_r, border_r)
|
|
|
|
.vert_line_by(box_height - 2 * border_r)
|
|
|
|
.arc_by(border_r, border_r, 0, false, true, -border_r, border_r)
|
|
|
|
.horiz_line_to(border_r)
|
|
|
|
.arc_by(border_r, border_r, 0, false, true, -border_r, -border_r)
|
|
|
|
.vert_line_to(border_r)
|
|
|
|
.arc_by(border_r, border_r, 0, false, true, border_r, -border_r);
|
|
|
|
g.push(
|
|
|
|
Path::new()
|
|
|
|
.with_stroke("#fff")
|
|
|
|
.with_stroke_width(3)
|
|
|
|
.with_fill("#000")
|
|
|
|
.with_fill_opacity(".3")
|
|
|
|
.with_d(outline)
|
|
|
|
);
|
|
|
|
g.push(
|
|
|
|
Text::new()
|
|
|
|
.with_x(question_offset + question_width / 2 + border_r)
|
|
|
|
.with_y(0)
|
|
|
|
.with_dominant_baseline("middle")
|
|
|
|
.with_text_anchor("middle")
|
|
|
|
.with_font_weight(600)
|
|
|
|
.append("Question")
|
|
|
|
);
|
|
|
|
g.push(text);
|
|
|
|
|
|
|
|
Self { res, g }
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn finish(self) -> Graphic {
|
|
|
|
let mut svg = Graphic::new();
|
|
|
|
svg.set_width(self.res.width());
|
|
|
|
svg.set_height(self.res.height());
|
|
|
|
svg.set_view_box("0 0 1920 1080");
|
|
|
|
svg.push(
|
|
|
|
Rect::new()
|
|
|
|
.with_fill("#040")
|
|
|
|
.with_x(0)
|
|
|
|
.with_y(0)
|
|
|
|
.with_width(1920)
|
|
|
|
.with_height(1080)
|
|
|
|
);
|
|
|
|
svg.push(self.g);
|
|
|
|
svg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
#[test]
|
|
|
|
fn question() {
|
|
|
|
let svg = Question::new(
|
|
|
|
Resolution::FullHD,
|
|
|
|
&Language::default(),
|
|
|
|
"Hallo Welt! Dies ist eine sehr kluge Frage aus dem Publikum. Die Frage ist nämlich: Was ist eigentlich die Frage?".into()
|
|
|
|
)
|
|
|
|
.finish();
|
|
|
|
std::fs::write("question.svg", svg.to_string_pretty()).unwrap();
|
|
|
|
}
|