use anyhow::Result;
use image::imageops::FilterType;
use std::{
f64::consts::SQRT_2,
path::{Path, PathBuf},
};
const MARK: &str = "cover-";
const SEEDS: u64 = 1_000_000;
const WIDTH: u32 = 1500;
const HEIGHT: u32 = WIDTH * 2 / 5;
pub const RASTER_BYTES: u64 = (WIDTH as u64 * 2) * (HEIGHT as u64 * 2) * 4;
const CELL: i64 = 4;
const BAND: i64 = 882;
const COLS: i64 = BAND / CELL;
const ROWS: i64 = COLS * 2 / 5;
const ORDERED: f64 = 0.75;
const FLOOR: f64 = 0.55;
const FALLOFF: f64 = 1.6;
const WOBBLE: f64 = 0.30;
const WOBBLE_SCALE: f64 = 5.0;
const ACCENT: f64 = 0.85;
const ACCENT_FALLOFF: f64 = 6.0;
const ACCENT_STRAY: f64 = 0.015;
const BAYER: [f64; 16] = [
0., 8., 2., 10., 12., 4., 14., 6., 3., 11., 1., 9., 15., 7., 13., 5.,
];
const GROUND: &str = "#ffffff";
const PAPER: [(&str, &str); 8] = [
("#ffb280", "#ff6600"),
("#a9c8ee", "#2c6fbb"),
("#b3d8b0", "#3e8e5a"),
("#f3b7c2", "#d6436e"),
("#c9b8e8", "#6b4fa8"),
("#f6d9a0", "#d9982b"),
("#a6d8d4", "#2e8b84"),
("#c2cbd4", "#5a6b7c"),
];
const ANCHORS: [(f64, f64); 2] = [(0., 0.), (1., 0.)];
pub fn of(article: &Path) -> Option<PathBuf> {
std::fs::read_dir(article.parent()?)
.ok()?
.flatten()
.map(|entry| entry.path())
.find(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(MARK))
})
}
pub fn path(article: &Path, seed: u64, ext: &str) -> Option<PathBuf> {
Some(article.with_file_name(format!("{MARK}{seed}.{ext}")))
}
pub fn seed(article: &Path, current: Option<&Path>) -> u64 {
let seed = match current.and_then(seed_of) {
Some(seed) => hash([seed, 1]),
None => hash(article.to_string_lossy().bytes().map(u64::from)),
};
seed % SEEDS
}
pub fn import(source: &Path, to: &Path) -> Result<()> {
let picture = image::open(source)?;
let picture = match picture.width() > WIDTH {
true => picture.resize(WIDTH, u32::MAX, FilterType::Lanczos3),
false => picture,
};
Ok(picture.save(to)?)
}
pub fn svg(seed: u64) -> String {
let (fill, accent) = PAPER[(seed % PAPER.len() as u64) as usize];
let anchor = ANCHORS[(seed / PAPER.len() as u64 % ANCHORS.len() as u64) as usize];
let (mut plain, mut hot) = (String::new(), String::new());
for row in 0..ROWS {
let mut run: Option<(i64, bool)> = None;
for col in 0..=COLS {
let here = (col < COLS).then(|| cell(col, row, seed, anchor)).flatten();
if let Some((start, was)) = run
&& here != Some(was)
{
let strip = match was {
true => &mut hot,
false => &mut plain,
};
strip.push_str(&format!(
"<rect x=\"{start}\" y=\"{row}\" width=\"{}\" height=\"1\"/>",
col - start
));
run = None;
}
if run.is_none()
&& let Some(is_accent) = here
{
run = Some((col, is_accent));
}
}
}
format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{WIDTH}\" height=\"{HEIGHT}\" \
viewBox=\"0 0 {COLS} {ROWS}\" shape-rendering=\"crispEdges\">\
<rect width=\"{COLS}\" height=\"{ROWS}\" fill=\"{GROUND}\"/>\
<g fill=\"{fill}\">{plain}</g><g fill=\"{accent}\">{hot}</g></svg>"
)
}
fn cell(col: i64, row: i64, seed: u64, anchor: (f64, f64)) -> Option<bool> {
let u = (col as f64 + 0.5) / COLS as f64;
let v = (row as f64 + 0.5) / ROWS as f64;
let away = (((u - anchor.0).powi(2) + (v - anchor.1).powi(2)).sqrt() / SQRT_2).min(1.);
let density =
FLOOR + (1. - FLOOR) * (1. - away).powf(FALLOFF) + (wobble(u, v, seed) - 0.5) * WOBBLE;
let threshold = ORDERED * BAYER[(row % 4 * 4 + col % 4) as usize] / 16.
+ (1. - ORDERED) * unit([col as u64, row as u64, seed]);
if threshold >= density {
return None;
}
let heat = ACCENT * (1. - away).powf(ACCENT_FALLOFF) + ACCENT_STRAY * (1. - away).powi(2);
Some(unit([col as u64, row as u64, seed, 7]) < heat)
}
fn seed_of(cover: &Path) -> Option<u64> {
let (_, tail) = cover.file_name()?.to_str()?.split_once(MARK)?;
tail.split_once('.')?.0.parse().ok()
}
fn wobble(u: f64, v: f64, seed: u64) -> f64 {
let (x, y) = (u * WOBBLE_SCALE, v * WOBBLE_SCALE);
let (x0, y0) = (x.floor(), y.floor());
let (fx, fy) = (smooth(x - x0), smooth(y - y0));
let at = |i: u64, j: u64| unit([x0 as u64 + i, y0 as u64 + j, seed]);
let top = at(0, 0) * (1. - fx) + at(1, 0) * fx;
let bottom = at(0, 1) * (1. - fx) + at(1, 1) * fx;
top * (1. - fy) + bottom * fy
}
fn smooth(t: f64) -> f64 {
t * t * (3. - 2. * t)
}
fn hash(keys: impl IntoIterator<Item = u64>) -> u64 {
let mut x = 0xcbf2_9ce4_8422_2325_u64;
for key in keys {
x = (x ^ key).wrapping_mul(0x0000_0100_0000_01b3);
}
x ^= x >> 33;
x = x.wrapping_mul(0xff51_afd7_ed55_8ccd);
x ^ (x >> 29)
}
fn unit(keys: impl IntoIterator<Item = u64>) -> f64 {
(hash(keys) % 1_000_003) as f64 / 1_000_003.
}