Merge branch 'ogp'
This commit is contained in:
commit
b16fbc9d78
|
@ -166,7 +166,6 @@ impl Preference for UserPreferencesV0 {
|
||||||
} else {
|
} else {
|
||||||
(i - last_default) as u8 - 2
|
(i - last_default) as u8 - 2
|
||||||
};
|
};
|
||||||
eprintln!("{} {} {} {}", toggle_enabled, distance, i, last_default);
|
|
||||||
commands.push(Command::Move {
|
commands.push(Command::Move {
|
||||||
toggle_enabled,
|
toggle_enabled,
|
||||||
distance
|
distance
|
||||||
|
@ -194,7 +193,6 @@ impl Preference for UserPreferencesV0 {
|
||||||
} else {
|
} else {
|
||||||
(i - last_enabled) as u8 - 2
|
(i - last_enabled) as u8 - 2
|
||||||
};
|
};
|
||||||
eprintln!("{} {} {} {}", toggle_enabled, distance, i, last_enabled);
|
|
||||||
commands.push(Command::Move {
|
commands.push(Command::Move {
|
||||||
toggle_enabled,
|
toggle_enabled,
|
||||||
distance
|
distance
|
||||||
|
|
42
src/util.rs
42
src/util.rs
|
@ -1,3 +1,21 @@
|
||||||
|
use time::{Date, Month, OffsetDateTime};
|
||||||
|
|
||||||
|
/// The start of the COVID-19 lockdowns
|
||||||
|
///
|
||||||
|
/// This is used as an epoch in order to convert from a given date to an integer seed. This is
|
||||||
|
/// specified as part of the algorithm for randomly selecting from a weighted list.
|
||||||
|
pub const COVID_EPOCH: Date = match Date::from_calendar_date(2020, Month::January, 26) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => Date::MIN, // This never runs, but we can't unwrap, so this is what we're stuck with
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A state to use as an initial state before seeding
|
||||||
|
///
|
||||||
|
/// Think of this as a ::new() method for a pcg64 generator. Subsiquent calls should be
|
||||||
|
/// made to the [`pcg64_seed()`] methods or the [`seed_with_date()`]/[`seed_with_today()`]
|
||||||
|
/// methods
|
||||||
|
pub const INITIAL_STATE: u128 = 1312_1312_1312;
|
||||||
|
|
||||||
pub fn pcg64(state: &mut u128) -> u64 {
|
pub fn pcg64(state: &mut u128) -> u64 {
|
||||||
let mut x = *state;
|
let mut x = *state;
|
||||||
pcg64_iterstate(state);
|
pcg64_iterstate(state);
|
||||||
|
@ -18,3 +36,27 @@ pub fn pcg64_seed(state: &mut u128, bytes: &[u8]) {
|
||||||
}
|
}
|
||||||
pcg64_iterstate(state);
|
pcg64_iterstate(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Seed the generator with a given [`Date`] object
|
||||||
|
pub fn seed_with_date(state: &mut u128, date: Date) {
|
||||||
|
pcg64_seed(
|
||||||
|
state,
|
||||||
|
&(
|
||||||
|
(date - COVID_EPOCH)
|
||||||
|
.whole_days()
|
||||||
|
as u32
|
||||||
|
).to_le_bytes()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seed the generator with the [`Date`] object representing today's date
|
||||||
|
///
|
||||||
|
/// Uses the system's local time, or falls back to UTC
|
||||||
|
pub fn seed_with_today(state: &mut u128) {
|
||||||
|
seed_with_date(
|
||||||
|
state,
|
||||||
|
OffsetDateTime::now_local()
|
||||||
|
.unwrap_or_else(|_| OffsetDateTime::now_utc())
|
||||||
|
.date()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +1,10 @@
|
||||||
use crate::{
|
use crate::{user_preferences::ParseError, util::{self, pcg64, pcg64_seed, seed_with_date, seed_with_today}};
|
||||||
user_preferences::ParseError,
|
|
||||||
util::{pcg64, pcg64_seed},
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::{collections::BTreeMap, hash::Hash};
|
use std::{collections::BTreeMap, hash::Hash};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::cmp::Eq;
|
use std::cmp::Eq;
|
||||||
|
|
||||||
use time::{Date, Month, OffsetDateTime};
|
use time::Date;
|
||||||
|
|
||||||
/// The start of the COVID-19 lockdowns
|
|
||||||
///
|
|
||||||
/// This is used as an epoch in order to convert from a given date to an integer seed. This is
|
|
||||||
/// specified as part of the algorithm for randomly selecting from a weighted list.
|
|
||||||
pub const COVID_EPOCH: Date = match Date::from_calendar_date(2020, Month::January, 26) {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => Date::MIN, // This never runs, but we can't unwrap, so this is what we're stuck with
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A list of pronouns and their associated weights, used for random selection
|
/// A list of pronouns and their associated weights, used for random selection
|
||||||
///
|
///
|
||||||
|
@ -82,41 +70,37 @@ impl<Loot: Eq + Ord + Hash + Copy + Debug> WeightedTable<Loot> {
|
||||||
///
|
///
|
||||||
/// The date is generated for the system's time and timezone
|
/// The date is generated for the system's time and timezone
|
||||||
pub fn select_today(&self, seed: &[u8]) -> Loot {
|
pub fn select_today(&self, seed: &[u8]) -> Loot {
|
||||||
self.select_on_date(
|
let mut generator = util::INITIAL_STATE;
|
||||||
seed,
|
|
||||||
OffsetDateTime::now_local()
|
seed_with_today(&mut generator);
|
||||||
.unwrap_or_else(|_| OffsetDateTime::now_utc())
|
pcg64_seed(&mut generator, seed);
|
||||||
.date()
|
|
||||||
)
|
self.select(&mut generator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Randomly select a pronoun set for a given date and name.
|
/// Randomly select a pronoun set for a given date and name.
|
||||||
///
|
///
|
||||||
/// Is a wrapper for calling [`WeightedTable::select`] with the given date mixed into the seed.
|
/// Is a wrapper for calling [`WeightedTable::select`] with the given date mixed into the seed.
|
||||||
pub fn select_on_date(&self, seed: &[u8], date: Date) -> Loot {
|
pub fn select_on_date(&self, seed: &[u8], date: Date) -> Loot {
|
||||||
let mut new_seed: Vec<u8> = Vec::with_capacity(seed.len() + 4);
|
let mut generator = util::INITIAL_STATE;
|
||||||
new_seed.extend(
|
|
||||||
(
|
seed_with_date(&mut generator, date);
|
||||||
(date - COVID_EPOCH)
|
pcg64_seed(&mut generator, seed);
|
||||||
.whole_days()
|
|
||||||
as u32
|
self.select(&mut generator)
|
||||||
).to_le_bytes()
|
|
||||||
);
|
|
||||||
new_seed.extend(seed);
|
|
||||||
self.select(&new_seed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Randomly select a pronoun set for a given seed
|
/// Randomly select a pronoun set from a given pre-seeded genenator
|
||||||
|
///
|
||||||
|
/// You're probably looking for [`select_today()`] or [`select_on_date()`]
|
||||||
///
|
///
|
||||||
/// This function is *pure*, and any randomness is produced internally using PRNG seeded with
|
/// This function is *pure*, and any randomness is produced internally using PRNG seeded with
|
||||||
/// the given date and seed. That is to say, for any given seed, this table must always
|
/// the given date and seed. That is to say, for any given seed, this table must always
|
||||||
/// produce the same pronoun set.
|
/// produce the same pronoun set.
|
||||||
pub fn select(&self, seed: &[u8]) -> Loot {
|
pub fn select(&self, generator: &mut u128) -> Loot {
|
||||||
let (rollable_table, sum_weights) = self.rollable_table();
|
let (rollable_table, sum_weights) = self.rollable_table();
|
||||||
|
|
||||||
let mut generator: u128 = 131213121312;
|
let random = pcg64(generator) % sum_weights;
|
||||||
pcg64_seed(&mut generator, seed);
|
|
||||||
let random = pcg64(&mut generator) % sum_weights;
|
|
||||||
|
|
||||||
rollable_table.iter()
|
rollable_table.iter()
|
||||||
.filter(|(weight, _)| random < *weight)
|
.filter(|(weight, _)| random < *weight)
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.otf filter=lfs diff=lfs merge=lfs -text
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pronouns-today-web"
|
name = "pronouns-today-web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
@ -11,3 +11,14 @@ actix-web = "3"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
askama = "0.10"
|
askama = "0.10"
|
||||||
|
rusttype = { version = "0.9.2", optional = true }
|
||||||
|
image = { version = "0.23.14", optional = true }
|
||||||
|
lazy_static = { version = "1.4.0", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Enables support for generating images for Open Graph Protocol cards. This means that
|
||||||
|
# when someone shares a link to their pronoun page on Twitter, Discord, or another app
|
||||||
|
# that supports OGP, the card will include a visual of their pronouns.
|
||||||
|
ogp_images = ["rusttype", "image", "lazy_static"]
|
||||||
|
|
||||||
|
default = ["ogp_images"]
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,72 @@
|
||||||
|
#![cfg(feature = "ogp_images")]
|
||||||
|
|
||||||
|
use image::{Pixel, Rgb, Rgba};
|
||||||
|
use pronouns_today::util::pcg64;
|
||||||
|
|
||||||
|
pub fn generate_color_pair(state: &mut u128) -> ([u8; 3], [u8; 3]) {
|
||||||
|
let mut background = random_color(state);
|
||||||
|
let mut foreground;
|
||||||
|
loop {
|
||||||
|
foreground = random_color(state);
|
||||||
|
if meets_criteria(&foreground, &background) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
background = random_color(state);
|
||||||
|
if meets_criteria(&foreground, &background) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(foreground, background)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_color_pair_rgba(state: &mut u128) -> (Rgba<u8>, Rgba<u8>) {
|
||||||
|
let (color1, color2) = generate_color_pair(state);
|
||||||
|
|
||||||
|
let (color1, color2): (Rgb<u8>, Rgb<u8>) = (
|
||||||
|
color1.into(),
|
||||||
|
color2.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
(color1.to_rgba(), color2.to_rgba())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn relative_lum(color: &[u8; 3]) -> f32 {
|
||||||
|
let color_mapped: Vec<f32> = color.iter()
|
||||||
|
.map(|val| {
|
||||||
|
let val = *val as f32 / 255.0;
|
||||||
|
if val < 0.03928 {
|
||||||
|
val / 12.92
|
||||||
|
} else {
|
||||||
|
((val+0.055)/1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return 0.2126 * color_mapped[0] + 0.7152 * color_mapped[1] + 0.0722 * color_mapped[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_contrast(color1: &[u8; 3], color2: &[u8; 3]) -> f32 {
|
||||||
|
let a = 0.05 + relative_lum(color1);
|
||||||
|
let b = 0.05 + relative_lum(color2);
|
||||||
|
(a / b).max(b / a)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_saturation(color: &[u8; 3]) -> f32 {
|
||||||
|
let chroma_max = *(color.iter().max().unwrap());
|
||||||
|
let chroma_min = *(color.iter().min().unwrap());
|
||||||
|
return ((chroma_max - chroma_min) as f32) / chroma_max as f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_color(state: &mut u128) -> [u8; 3] {
|
||||||
|
let random = pcg64(state).to_le_bytes();
|
||||||
|
return [
|
||||||
|
random[0],
|
||||||
|
random[1],
|
||||||
|
random[2],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn meets_criteria(foreground: &[u8; 3], background: &[u8; 3]) -> bool {
|
||||||
|
return check_contrast(foreground, background) >= 5.0 &&
|
||||||
|
get_saturation(background) < 0.65 &&
|
||||||
|
get_saturation(foreground) < 0.65
|
||||||
|
}
|
113
web/src/main.rs
113
web/src/main.rs
|
@ -1,3 +1,6 @@
|
||||||
|
pub mod ogp_images;
|
||||||
|
pub mod contrast;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
@ -5,24 +8,35 @@ use actix_web::dev::HttpResponseBuilder;
|
||||||
use actix_web::http::{header, StatusCode};
|
use actix_web::http::{header, StatusCode};
|
||||||
use actix_web::middleware::normalize::TrailingSlash;
|
use actix_web::middleware::normalize::TrailingSlash;
|
||||||
use actix_web::middleware::{self, Logger};
|
use actix_web::middleware::{self, Logger};
|
||||||
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, ResponseError, Result};
|
use actix_web::web::resource;
|
||||||
|
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, ResponseError, Result, post, web};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use pronouns_today::user_preferences::Preference;
|
use pronouns_today::user_preferences::Preference;
|
||||||
use pronouns_today::{InstanceSettings, Pronoun};
|
use pronouns_today::{InstanceSettings, Pronoun};
|
||||||
|
|
||||||
|
#[cfg(feature = "ogp_images")]
|
||||||
|
use image::{DynamicImage, ImageOutputFormat};
|
||||||
|
#[cfg(feature = "ogp_images")]
|
||||||
|
use ogp_images::render_today;
|
||||||
|
|
||||||
|
// TODO: Make this configurable
|
||||||
|
const HOSTNAME: &str = "pronouns.today";
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
struct IndexTemplate<'a> {
|
struct IndexTemplate<'a> {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
pronoun: &'a Pronoun,
|
pronoun: &'a Pronoun,
|
||||||
pronouns: Vec<(usize, &'a Pronoun)>,
|
pronouns: Vec<(usize, &'a Pronoun)>,
|
||||||
|
url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_page(pronoun: &Pronoun, settings: &InstanceSettings, name: Option<String>) -> String {
|
fn render_page(pronoun: &Pronoun, settings: &InstanceSettings, name: Option<String>, url: String) -> String {
|
||||||
IndexTemplate {
|
IndexTemplate {
|
||||||
name,
|
name,
|
||||||
pronoun,
|
pronoun,
|
||||||
pronouns: settings.pronoun_list.iter().enumerate().collect(),
|
pronouns: settings.pronoun_list.iter().enumerate().collect(),
|
||||||
|
url,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -53,40 +67,67 @@ async fn create_link(
|
||||||
.finish())
|
.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
fn form_full_url(host: &str, name: Option<&str>, prefstr: Option<&str>) -> String {
|
||||||
async fn default(settings: web::Data<InstanceSettings>) -> Result<impl Responder> {
|
["https:/", host].into_iter()
|
||||||
let pronoun = settings
|
.chain(name)
|
||||||
.select_pronouns(None, None)
|
.chain(prefstr)
|
||||||
.map_err(|_| Error::InvlaidPrefString)?;
|
.collect::<Vec<&str>>()
|
||||||
Ok(HttpResponse::Ok()
|
.join("/")
|
||||||
.content_type("text/html; charset=utf-8")
|
|
||||||
.body(render_page(&pronoun, &settings, None)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/{prefs}")]
|
/// Determine some basic information about a request
|
||||||
async fn only_prefs(
|
///
|
||||||
web::Path(prefs): web::Path<String>,
|
/// Determines the name and prefstring properties, if available, and also computes the pronoun that
|
||||||
settings: web::Data<InstanceSettings>,
|
/// should be responded using. This method is designed to facilitate creating the basic response
|
||||||
) -> Result<impl Responder> {
|
/// pages.
|
||||||
|
///
|
||||||
|
/// Both arguments should be passed directly from the caller, and the return values are, in order,
|
||||||
|
/// the prefstring, if available, the user's name, if available, and the computed pronouns.
|
||||||
|
fn get_request_info<'s, 'r>(
|
||||||
|
settings: &'s web::Data<InstanceSettings>,
|
||||||
|
req: &'r HttpRequest,
|
||||||
|
) -> (Option<&'r str>, Option<&'r str>, Result<&'s Pronoun, Error>) {
|
||||||
|
let prefs = req.match_info().get("prefs");
|
||||||
|
let name = req.match_info().get("name");
|
||||||
let pronoun = settings
|
let pronoun = settings
|
||||||
.select_pronouns(None, Some(&prefs))
|
.select_pronouns(name, prefs)
|
||||||
.map_err(|_| Error::InvlaidPrefString)?;
|
.map_err(|_| Error::InvlaidPrefString);
|
||||||
Ok(HttpResponse::Ok()
|
(prefs, name, pronoun)
|
||||||
.content_type("text/html; charset=utf-8")
|
|
||||||
.body(render_page(&pronoun, &settings, None)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/{name}/{prefs}")]
|
async fn handle_basic_request(
|
||||||
async fn prefs_and_name(
|
|
||||||
web::Path((name, prefs)): web::Path<(String, String)>,
|
|
||||||
settings: web::Data<InstanceSettings>,
|
settings: web::Data<InstanceSettings>,
|
||||||
|
req: HttpRequest,
|
||||||
) -> Result<impl Responder> {
|
) -> Result<impl Responder> {
|
||||||
let pronoun = settings
|
|
||||||
.select_pronouns(Some(&name), Some(&prefs))
|
let (prefstr, name, pronoun) = get_request_info(&settings, &req);
|
||||||
.map_err(|_| Error::InvlaidPrefString)?;
|
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type("text/html; charset=utf-8")
|
.content_type("text/html; charset=utf-8")
|
||||||
.body(render_page(&pronoun, &settings, Some(name))))
|
.body(render_page(
|
||||||
|
pronoun?,
|
||||||
|
&settings,
|
||||||
|
name.map(str::to_owned),
|
||||||
|
form_full_url(HOSTNAME, name, prefstr)
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ogp_images")]
|
||||||
|
async fn handle_thumbnail_request(
|
||||||
|
settings: web::Data<InstanceSettings>,
|
||||||
|
req: HttpRequest,
|
||||||
|
) -> Result<impl Responder> {
|
||||||
|
|
||||||
|
let (_, name, pronoun) = get_request_info(&settings, &req);
|
||||||
|
|
||||||
|
let mut data: Vec<u8> = Vec::with_capacity(15_000);
|
||||||
|
let image = DynamicImage::ImageRgb8(render_today(pronoun?, name.unwrap_or("")));
|
||||||
|
image.write_to(&mut data, ImageOutputFormat::Png)
|
||||||
|
.expect("Error encoding thumbnail to PNG");
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type("image/png")
|
||||||
|
.body(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn not_found() -> impl Responder {
|
async fn not_found() -> impl Responder {
|
||||||
|
@ -101,13 +142,21 @@ async fn main() -> std::io::Result<()> {
|
||||||
println!("Starting pronouns-today-web on 127.0.0.1:8080");
|
println!("Starting pronouns-today-web on 127.0.0.1:8080");
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
let logger = Logger::default();
|
let logger = Logger::default();
|
||||||
App::new()
|
let app = App::new()
|
||||||
.data(InstanceSettings::default())
|
.data(InstanceSettings::default())
|
||||||
.wrap(logger)
|
.wrap(logger)
|
||||||
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
|
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim));
|
||||||
.service(prefs_and_name)
|
|
||||||
.service(only_prefs)
|
#[cfg(feature = "ogp_images")]
|
||||||
.service(default)
|
let app = app
|
||||||
|
.service(resource("/thumb.png") .to(handle_thumbnail_request))
|
||||||
|
.service(resource("/{prefs}/thumb.png") .to(handle_thumbnail_request))
|
||||||
|
.service(resource("/{name}/{prefs}/thumb.png").to(handle_thumbnail_request));
|
||||||
|
|
||||||
|
app
|
||||||
|
.service(resource("/") .to(handle_basic_request))
|
||||||
|
.service(resource("/{prefs}") .to(handle_basic_request))
|
||||||
|
.service(resource("/{name}/{prefs}").to(handle_basic_request))
|
||||||
.service(create_link)
|
.service(create_link)
|
||||||
.default_service(web::to(not_found))
|
.default_service(web::to(not_found))
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
#![cfg(feature = "ogp_images")]
|
||||||
|
|
||||||
|
use crate::contrast::generate_color_pair_rgba;
|
||||||
|
|
||||||
|
use image::{DynamicImage, ImageBuffer, Pixel, Rgb, Rgba};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use pronouns_today::{Pronoun, util::{self, pcg64_seed, seed_with_today}};
|
||||||
|
use rusttype::{Font, Scale, point};
|
||||||
|
|
||||||
|
const SCALE: u32 = 400;
|
||||||
|
const FONT_SCALE: f32 = 250.0;
|
||||||
|
const FONT_DATA: &[u8] = include_bytes!("../assets/LeagueGothic.otf");
|
||||||
|
lazy_static! {
|
||||||
|
static ref FONT: Font<'static> = Font::try_from_bytes(FONT_DATA)
|
||||||
|
.expect("Invalid font data in build. This build and binary is invalid, and cannot be used.");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_today(pronouns: &Pronoun, name: &str) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
|
||||||
|
let mut state = util::INITIAL_STATE;
|
||||||
|
|
||||||
|
seed_with_today(&mut state);
|
||||||
|
pcg64_seed(&mut state, name.as_bytes());
|
||||||
|
|
||||||
|
render(pronouns, &mut state)
|
||||||
|
}
|
||||||
|
pub fn render(pronouns: &Pronoun, pcg64: &mut u128) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
|
||||||
|
let (color1, color2) = generate_color_pair_rgba(pcg64);
|
||||||
|
|
||||||
|
render_with_colors(pronouns, color1, color2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_with_colors(pronouns: &Pronoun, color1: Rgba<u8>, color2: Rgba<u8>) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
|
||||||
|
|
||||||
|
let scale = Scale::uniform(FONT_SCALE);
|
||||||
|
|
||||||
|
let mut output_image = ImageBuffer::from_fn(SCALE * 2, SCALE, |x, _| {
|
||||||
|
if x < SCALE {
|
||||||
|
color1
|
||||||
|
} else {
|
||||||
|
color2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (color, text, offset) in [(color2, &pronouns.subject_pronoun, 0), (color1, &pronouns.object_pronoun, 400)] {
|
||||||
|
let v_metrics = FONT.v_metrics(scale);
|
||||||
|
let glyphs: Vec<_> = FONT
|
||||||
|
.layout(&text.to_uppercase(), scale, point(0.0, v_metrics.ascent))
|
||||||
|
.collect();
|
||||||
|
let min_x = glyphs
|
||||||
|
.first()
|
||||||
|
.map(|g| g.pixel_bounding_box().unwrap().min.x)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let max_x = glyphs
|
||||||
|
.last()
|
||||||
|
.map(|g| g.pixel_bounding_box().unwrap().max.x)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let width = max_x - min_x;
|
||||||
|
let height = v_metrics.ascent - v_metrics.descent;
|
||||||
|
|
||||||
|
// Recompute offset to center text on page
|
||||||
|
let x_offset = (SCALE as u32 - width as u32) / 2 + offset;
|
||||||
|
let y_offset = ((SCALE as f32 - height) / 2.0) as u32;
|
||||||
|
|
||||||
|
// Draw glyphs onto the buffer
|
||||||
|
for glyph in glyphs {
|
||||||
|
if let Some(bounding_box) = glyph.pixel_bounding_box() {
|
||||||
|
glyph.draw(|x, y, v| {
|
||||||
|
if v > 0.0 {
|
||||||
|
|
||||||
|
let mut overlay_color = color;
|
||||||
|
let alpha = &mut overlay_color.channels_mut()[3];
|
||||||
|
*alpha = (*alpha as f32 * v) as u8;
|
||||||
|
|
||||||
|
output_image.get_pixel_mut(
|
||||||
|
x + x_offset + bounding_box.min.x as u32,
|
||||||
|
y + y_offset + bounding_box.min.y as u32
|
||||||
|
).blend(&overlay_color);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicImage::ImageRgba8(output_image).into_rgb8()
|
||||||
|
}
|
|
@ -2,6 +2,24 @@
|
||||||
<head>
|
<head>
|
||||||
<title>{% if name.is_some() %} {{ name.as_ref().unwrap() }}'s {% else %}
|
<title>{% if name.is_some() %} {{ name.as_ref().unwrap() }}'s {% else %}
|
||||||
My{% endif %} Pronouns Today</title>
|
My{% endif %} Pronouns Today</title>
|
||||||
|
|
||||||
|
<!-- OGP Tags -->
|
||||||
|
{% if name.is_some() %}
|
||||||
|
<meta property="og:title" content="{{ name.as_ref().unwrap() }}'s Pronouns Today" />
|
||||||
|
{% else %}
|
||||||
|
<meta property="og:title" content="Random Daily Pronouns" />
|
||||||
|
{% endif %}
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="og:site_name" content="Pronouns.Today" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="{{ url }}" />
|
||||||
|
<meta property="og:image:url" content="{{ url }}/thumb.png" />
|
||||||
|
<meta property="og:image:type" content="image/png" />
|
||||||
|
<meta property="og:image:width" content="800" />
|
||||||
|
<meta property="og:image:height" content="400" />
|
||||||
|
<meta property="og:image:alt" content="The pronouns {{pronoun.subject_pronoun | capitalize}}/{{pronoun.object_pronoun | capitalize}}" />
|
||||||
|
<meta property="og:description" content="{{ name.as_ref().map(String::as_str).unwrap_or("Whoever posted this") }} uses any pronouns, but for today, {{pronoun.subject_pronoun}} would appreciate it if you used {{pronoun.render_threeform()}} pronouns for {{pronoun.object_pronoun}}." />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#form {
|
#form {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
Loading…
Reference in New Issue