Typstify /egg endpoint

This commit is contained in:
Joscha 2025-03-01 18:32:56 +01:00
parent 6d4db1ca2e
commit db06addc42
12 changed files with 141 additions and 112 deletions

View file

@ -1,5 +1,6 @@
use showbits_typst::Typst;
pub mod egg;
pub mod image;
pub mod text;

View file

@ -0,0 +1,9 @@
{
"covers": 7,
"patterns": 40,
"bad_covers": 1,
"bad_patterns": 7,
"seed": 1,
"mode": null,
"feed": true
}

View file

@ -0,0 +1 @@
../../../../showbits-assets/data/egg

View file

@ -0,0 +1 @@
../../../../showbits-assets/data/egg_bad

View file

@ -0,0 +1 @@
../lib.typ

View file

@ -0,0 +1,55 @@
#import "@preview/oxifmt:0.2.1": strfmt
#import "@preview/suiji:0.3.0": *
#import "lib.typ";
#show: it => lib.init(it)
#let data = json("data.json")
#let rng = gen-rng(data.seed)
#let file_series(n, fmt) = array.range(n).map(n => strfmt(fmt, n))
#let good_covers = file_series(data.covers, "egg/cover_{:02}.png")
#let good_patterns = file_series(data.patterns, "egg/pattern_{:02}.png")
#let bad_covers = file_series(data.bad_covers, "egg_bad/cover_{:02}.png")
#let bad_patterns = file_series(data.bad_patterns, "egg_bad/pattern_{:02}.png")
// Always generate random value to so that egg looks the same whether we chose
// the mode directly or randomly.
#let (rng, val) = random(rng)
#let (covers, patterns) = if data.mode == "good" {
(good_covers, good_patterns)
} else if data.mode == "bad" {
(bad_covers, bad_patterns)
} else if val < 1 / 8 {
(bad_covers, bad_patterns)
} else {
(good_covers, good_patterns)
}
#context {
let (rng, cover) = choice(rng, covers)
let cover = image(cover, width: lib.width)
let cover_size = measure(cover)
let pattern_stack_height = 0pt
let pattern_stack = while pattern_stack_height < cover_size.height {
let pattern = ()
(rng, pattern) = choice(rng, patterns)
let pattern = image(pattern, width: cover_size.width)
pattern_stack_height += measure(pattern).height
(pattern,)
}
box(
height: cover_size.height,
clip: true,
stack(dir: ttb, ..pattern_stack),
)
place(top + left, cover)
}
#if data.feed {
lib.feed
}

View file

@ -0,0 +1,64 @@
use axum::{Form, extract::State};
use serde::{Deserialize, Serialize};
use crate::{
drawer::{Command, NewTypstDrawing},
server::Server,
};
#[derive(Serialize)]
struct Data {
covers: usize,
patterns: usize,
bad_covers: usize,
bad_patterns: usize,
seed: i64,
mode: Option<String>,
feed: bool,
}
#[derive(Deserialize)]
pub struct FormData {
pub seed: Option<i64>,
pub mode: Option<String>,
pub feed: Option<bool>,
}
pub async fn post(server: State<Server>, Form(form): Form<FormData>) {
let seed = form.seed.unwrap_or_else(rand::random);
let data = Data {
covers: showbits_assets::EGG_COVERS.len(),
patterns: showbits_assets::EGG_PATTERNS.len(),
bad_covers: showbits_assets::EGG_BAD_COVERS.len(),
bad_patterns: showbits_assets::EGG_BAD_PATTERNS.len(),
seed,
mode: form.mode,
feed: form.feed.unwrap_or(true),
};
let mut typst = super::typst_with_lib()
.with_json("/data.json", &data)
.with_main_file(include_str!("main.typ"));
for (i, cover) in showbits_assets::EGG_COVERS.iter().enumerate() {
typst.add_file(format!("/egg/cover_{i:02}.png"), *cover);
}
for (i, pattern) in showbits_assets::EGG_PATTERNS.iter().enumerate() {
typst.add_file(format!("/egg/pattern_{i:02}.png"), *pattern);
}
for (i, cover) in showbits_assets::EGG_BAD_COVERS.iter().enumerate() {
typst.add_file(format!("/egg_bad/cover_{i:02}.png"), *cover);
}
for (i, pattern) in showbits_assets::EGG_BAD_PATTERNS.iter().enumerate() {
typst.add_file(format!("/egg_bad/pattern_{i:02}.png"), *pattern);
}
let _ = server
.tx
.send(Command::draw(NewTypstDrawing::new(typst)))
.await;
}

View file

@ -0,0 +1 @@
../plugin.wasm

View file

@ -1,6 +1,8 @@
#let width = 384pt
#let init(it) = {
set page(
width: 384pt,
width: width,
height: auto,
margin: (x: 0pt, y: 4pt),
)

View file

@ -2,7 +2,6 @@ mod backlog;
mod calendar;
mod cells;
mod chat_message;
mod egg;
mod new_typst;
mod photo;
mod tictactoe;
@ -15,8 +14,8 @@ use crate::persistent_printer::PersistentPrinter;
pub use self::{
backlog::BacklogDrawing, calendar::CalendarDrawing, cells::CellsDrawing,
chat_message::ChatMessageDrawing, egg::EggDrawing, new_typst::NewTypstDrawing,
photo::PhotoDrawing, tictactoe::TicTacToeDrawing, typst::TypstDrawing,
chat_message::ChatMessageDrawing, new_typst::NewTypstDrawing, photo::PhotoDrawing,
tictactoe::TicTacToeDrawing, typst::TypstDrawing,
};
pub const FEED: f32 = 96.0;

View file

@ -1,98 +0,0 @@
use image::{RgbaImage, imageops};
use rand::{Rng, seq::IndexedRandom};
use showbits_assets::{EGG_BAD_COVERS, EGG_BAD_PATTERNS, EGG_COVERS, EGG_PATTERNS};
use showbits_common::{
Node, Tree, WidgetExt,
color::{self, WHITE},
widgets::{Image, Text},
};
use taffy::{AlignItems, Display, FlexDirection, prelude::length, style_helpers::percent};
use crate::persistent_printer::PersistentPrinter;
use super::{Context, Drawing, FEED};
pub struct EggDrawing;
fn load_image(bytes: &[u8]) -> RgbaImage {
image::load_from_memory(bytes)
.expect("malformed image data")
.into_rgba8()
}
impl Drawing for EggDrawing {
fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> {
let mut rng = rand::rng();
// Choose which set of egg images to use
let bad_egg = rng.random_range(0..8) == 0;
let (covers, patterns) = if bad_egg {
(EGG_BAD_COVERS, EGG_BAD_PATTERNS)
} else {
(EGG_COVERS, EGG_PATTERNS)
};
// Load images from memory
let covers = covers.iter().map(|img| load_image(img)).collect::<Vec<_>>();
let patterns = patterns
.iter()
.map(|img| load_image(img))
.collect::<Vec<_>>();
// Choose a random cover
let cover = covers.choose(&mut rng).expect("too few covers");
// Prepare image of appropriate size
let mut image =
RgbaImage::from_pixel(cover.width(), cover.height(), color::to_image_color(WHITE));
// Draw patterns onto egg
let mut last_idx = None;
let mut y = rng.random_range(-100_i64..0);
let height: i64 = image.height().into();
while y < height {
let idx = loop {
let idx = rng.random_range(0..patterns.len());
if Some(idx) != last_idx {
break idx;
}
};
let paint = &patterns[idx];
imageops::overlay(&mut image, paint, 0, y);
y += <_ as Into<i64>>::into(paint.height());
last_idx = Some(idx);
}
// Finally, draw the cover
imageops::overlay(&mut image, cover, 0, 0);
let mut tree = Tree::<Context>::new(WHITE);
let image = Image::new(image)
.with_grow(false)
.with_shrink(false)
.node()
.register(&mut tree)?;
let text = Text::new()
.with_metrics(Text::default_metrics().scale(2.0))
.and_plain("Frohe Ostern!")
.widget(&mut ctx.font_stuff)
.node()
.register(&mut tree)?;
let root = Node::empty()
.with_size_width(percent(1.0))
.with_padding_bottom(length(FEED))
.with_display(Display::Flex)
.with_flex_direction(FlexDirection::Column)
.with_align_items(Some(AlignItems::Center))
.and_child(image)
.and_child(text)
.register(&mut tree)?;
printer.print_tree(&mut tree, ctx, root)?;
Ok(())
}
}

View file

@ -15,8 +15,8 @@ use tokio::{net::TcpListener, sync::mpsc};
use crate::{
documents,
drawer::{
CalendarDrawing, CellsDrawing, ChatMessageDrawing, Command, EggDrawing, PhotoDrawing,
TicTacToeDrawing, TypstDrawing,
CalendarDrawing, CellsDrawing, ChatMessageDrawing, Command, PhotoDrawing, TicTacToeDrawing,
TypstDrawing,
},
};
@ -32,7 +32,7 @@ pub async fn run(tx: mpsc::Sender<Command>, addr: String) -> anyhow::Result<()>
.route("/calendar", post(post_calendar))
.route("/cells", post(post_cells))
.route("/chat_message", post(post_chat_message))
.route("/egg", post(post_egg).fallback(get_static_file))
.route("/egg", post(documents::egg::post).fallback(get_static_file))
.route(
"/image",
post(documents::image::post).fallback(get_static_file),
@ -109,13 +109,6 @@ async fn post_chat_message(server: State<Server>, request: Form<PostChatMessageF
.await;
}
// /egg
async fn post_egg(server: State<Server>) -> impl IntoResponse {
let _ = server.tx.send(Command::draw(EggDrawing)).await;
Redirect::to("egg")
}
// /photo
async fn post_photo(server: State<Server>, mut multipart: Multipart) -> somehow::Result<Response> {