diff --git a/snuggle.toml b/snuggle.toml index e4009fa..324a5fa 100644 --- a/snuggle.toml +++ b/snuggle.toml @@ -1,4 +1,31 @@ name = "snuggle" -dx = -60 -dy = -40 -transform = "scale(1.02 1.02) translate(-1.5 -1)" +dx = -90 +dy = -30 +bold = 12.0 + +# TODO: (medium) read from the species declaration and grab all the svgs with a given tag +# TODO: (low) generate the SVGs in-memory instead of reading them from the disk +[left] +blobfox = "blobfox_snuggle_left" +blobfox_blush = "blobfox_blush" +blobfox_happy = "blobfox_happy" +blobfox_aww = "blobfox_aww" + +blobcat = "blobcat_snuggle_left" +blobcat_blush = "blobcat_blush" +blobcat_happy = "blobcat_happy" +blobcat_aww = "blobcat_aww" + +blobstella = "blobstella_snuggle_left" +blobstella_blush = "blobstella_blush" +blobstella_happy = "blobstella_happy" +blobstella_aww = "blobstella_aww" + +blobarcticfox = "blobarcticfox_snuggle_left" +blobarcticfox_blush = "blobarcticfox_blush" +blobarcticfox_happy = "blobarcticfox_happy" +blobarcticfox_aww = "blobarcticfox_aww" + +[right] +blobfox = "blobfox_snuggle_right" +blobfox_blush = "blobfox_snuggle_right_blush" diff --git a/species/blobfox/species.toml b/species/blobfox/species.toml index abf0481..69951f6 100644 --- a/species/blobfox/species.toml +++ b/species/blobfox/species.toml @@ -14,6 +14,7 @@ base = ["body-basic", "eyes-basic", "mouth-w"] happy = ["body-basic", "eyes-happy", "mouth-w"] evil = ["body-basic", "eyes-evil", "mouth-w"] owo = ["body-basic", "ear-owo", "eyes-owo", "mouth-w"] +aww = ["body-basic", "eyes-aww", "mouth-w"] "3c" = ["body-basic", "eyes-basic", "mouth-w", "hand-3c", "left-hand"] "3c_evil" = ["body-basic", "eyes-evil", "mouth-w", "hand-3c", "left-hand"] @@ -54,5 +55,6 @@ heart_demisexual = ["body-basic", "eyes-basic", "left-hand", "right-hand", "hold heart_pan = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding", "big-object"] # Snuggle +snuggle_left = ["body-basic", "eyes-closed", "mouth-w"] snuggle_right = ["body-snuggle", "eyes-snuggle", "mouth-w"] -snuggle_right_shadow = ["body-snuggle", "eyes-snuggle", "mouth-w"] +snuggle_right_blush = ["body-snuggle", "eyes-snuggle", "mouth-w", "blush"] diff --git a/species/blobfox/templates/nose.mustache b/species/blobfox/templates/nose.mustache index 7bb93d6..2377355 100644 --- a/species/blobfox/templates/nose.mustache +++ b/species/blobfox/templates/nose.mustache @@ -8,6 +8,9 @@ {{#tags.eyes-aww}} {{#reach_aww}}#nose-outline{{/reach_aww}} {{/tags.eyes-aww}} + {{#tags.eyes-happy}} + {{#snug}}#nose-outline{{/snug}} + {{/tags.eyes-happy}} {{#tags.eyes-evil}} {{#3c_evil}}#nose-outline{{/3c_evil}} {{/tags.eyes-evil}} diff --git a/species/blobfox/variants/aww.mustache b/species/blobfox/variants/aww.mustache new file mode 100644 index 0000000..be8b98b --- /dev/null +++ b/species/blobfox/variants/aww.mustache @@ -0,0 +1,7 @@ +{{>header}} + {{>body}} + + {{>eyes}} + {{>nose}} + {{>mouth}} +{{>footer}} diff --git a/species/blobfox/variants/snuggle_left.mustache b/species/blobfox/variants/snuggle_left.mustache new file mode 100644 index 0000000..be8b98b --- /dev/null +++ b/species/blobfox/variants/snuggle_left.mustache @@ -0,0 +1,7 @@ +{{>header}} + {{>body}} + + {{>eyes}} + {{>nose}} + {{>mouth}} +{{>footer}} diff --git a/species/blobfox/variants/snuggle_right_blush.mustache b/species/blobfox/variants/snuggle_right_blush.mustache new file mode 100644 index 0000000..a6a9544 --- /dev/null +++ b/species/blobfox/variants/snuggle_right_blush.mustache @@ -0,0 +1,10 @@ +{{>header}} + {{>body}} + + + {{>eyes}} + {{>nose}} + {{>mouth}} + {{>blush}} + +{{>footer}} diff --git a/src/bin/snuggle.rs b/src/bin/snuggle.rs index d4cb8b0..1a0bc3d 100644 --- a/src/bin/snuggle.rs +++ b/src/bin/snuggle.rs @@ -2,6 +2,7 @@ use clap::Parser; use std::fmt::Write; use std::path::PathBuf; +use std::collections::HashMap; use serde::{Serialize, Deserialize}; use xmltree::{Element, XMLNode}; @@ -13,48 +14,82 @@ use blobfox_template::{ #[derive(Serialize, Deserialize, Debug)] struct Desc { + /// Name of the snuggle emote (eg. `snuggle`, `nom`) + name: String, + + /// How much to move the "left" emote by, horizontally dx: f64, + /// How much to move the "left" emote by, vertically dy: f64, - + /// How much to scale the "left" emote by, unimplemented! scale: Option, + /// How much of a margin to add to the "right" emote, in SVG units + bold: f64, + + /// Optional transform to add to the "right" emote cutout #[serde(default)] transform: String, + + /// name/filename list of emotes for the "left" emotes + left: HashMap, + /// name/filename list of emotes for the "right" emotes + right: HashMap, } fn main() { let args = Args::parse(); + let input_dir = args.input_dir.clone().unwrap_or(PathBuf::from("output/vector/")); + let output_dir = args.output_dir.clone().unwrap_or(PathBuf::from("output/")); - let left = std::fs::read_to_string(&args.input_left).unwrap_or_else(|err| { - panic!("Couldn't open {}: {}", args.input_right.display(), err); - }); - let right = std::fs::read_to_string(&args.input_right).unwrap_or_else(|err| { - panic!("Couldn't open {}: {}", args.input_right.display(), err); - }); + let files = std::fs::read_dir(&input_dir).unwrap_or_else(|err| { + panic!("Couldn't read directory {}: {}", input_dir.display(), err); + }).filter_map(|entry| { + let entry = entry.ok()?; + Some((entry.path().file_stem()?.to_str()?.to_string(), entry.path())) + }).collect::>(); let desc = std::fs::read_to_string(&args.desc).unwrap_or_else(|err| { panic!("Couldn't open {}: {}", args.desc.display(), err); }); let desc: Desc = toml::from_str(&desc).unwrap(); - let snuggle = generate_snuggle(left, right, desc); - let snuggle = export::xml_to_str(&snuggle).unwrap(); - - let output_dir = args.output_dir.clone().unwrap_or(PathBuf::from("output/")); - - export::export( - snuggle, - &output_dir, - args.name.clone(), - &args.clone().into() - ).unwrap(); + let export_args: export::ExportArgs = args.clone().into(); + + for (left_name, left_path) in desc.left.iter() { + if let Some(left_path) = files.get(left_path) { + let left = std::fs::read_to_string(left_path).unwrap_or_else(|err| { + panic!("Couldn't open {}: {}", left_path.display(), err); + }); + + for (right_name, right_path) in desc.right.iter() { + if let Some(right_path) = files.get(right_path) { + let right = std::fs::read_to_string(&right_path).unwrap_or_else(|err| { + panic!("Couldn't open {}: {}", right_path.display(), err); + }); + + let snuggle = generate_snuggle(&left, &right, &desc); + let snuggle = export::xml_to_str(&snuggle).unwrap(); + + let name = format!("{}_{}_{}", left_name, desc.name, right_name); + + export::export( + snuggle, + &output_dir, + name, + &export_args + ).unwrap(); + } + } + } + } } -fn generate_snuggle(left: String, right: String, desc: Desc) -> Element { +fn generate_snuggle(left: &str, right: &str, desc: &Desc) -> Element { let left_usvg = export::get_usvg(&left).unwrap(); let left_bbox = left_usvg.svg_node().view_box.rect; - // + // == Generate mask == let mut mask = Element::new("mask"); mask.attributes.insert("id".to_string(), "snuggle-mask".to_string()); @@ -69,9 +104,10 @@ fn generate_snuggle(left: String, right: String, desc: Desc) -> Element { mask.children.push(XMLNode::Element(rect)); let mut right_mask = Element::new("g"); - right_mask.attributes.insert("transform".to_string(), desc.transform); + right_mask.attributes.insert("transform".to_string(), desc.transform.clone()); let mut right_xml = Element::parse(right.as_bytes()).unwrap(); + bolden(desc.bold, &mut right_xml); template::set_fill("#000000", &mut right_xml); template::set_stroke("#000000", &mut right_xml); @@ -83,6 +119,7 @@ fn generate_snuggle(left: String, right: String, desc: Desc) -> Element { mask.children.push(XMLNode::Element(right_mask)); + // == Insert both emotes == let mut right_xml = Element::parse(right.as_bytes()).unwrap(); let left_xml = Element::parse(left.as_bytes()).unwrap(); @@ -98,6 +135,7 @@ fn generate_snuggle(left: String, right: String, desc: Desc) -> Element { left_group2.attributes.insert("mask".to_string(), "url(#snuggle-mask)".to_string()); left_group2.children.push(XMLNode::Element(left_group)); + // == Fill in root element == let mut res = Element::new("svg"); res.attributes.insert("xmlns".to_string(), "http://www.w3.org/2000/svg".to_string()); res.attributes.insert("version".to_string(), "1.1".to_string()); @@ -110,15 +148,51 @@ fn generate_snuggle(left: String, right: String, desc: Desc) -> Element { res } +/// Increases the `stroke-width` of any drawn element by `amount`. +/// If the element has no stroke but has a filling, then it is considered to have a zero stroke width +fn bolden(amount: f64, xml: &mut Element) { + if let Some(stroke_width) = xml.attributes.get_mut("stroke-width") { + if let Ok(parsed) = stroke_width.parse::() { + *stroke_width = format!("{}", parsed + amount); + } + } else if xml.attributes.contains_key("fill") { + xml.attributes.insert("stroke-width", amount.to_string()); + } + + if let Some(style) = xml.attributes.get_mut("style") { + let mut new_style = String::new(); + let mut stroke_width = None; + for (name, value) in parse::parse_css(style) { + if name == "stroke-width" { + stroke_width = value.parse::().ok(); + continue + } + + if name == "fill" && stroke_width.is_none() { + stroke_width = Some(0.0); + } + + write!(&mut new_style, "{}:{};", name, value).unwrap(); + } + + if let Some(stroke_width) = stroke_width { + write!(&mut new_style, "stroke-width: {};", stroke_width + amount).unwrap(); + } + + *style = new_style; + } + + for child in xml.children.iter_mut() { + if let XMLNode::Element(ref mut child) = child { + bolden(amount, child); + } + } +} + #[derive(Parser, Clone)] #[clap(author, version, about, long_about = None)] struct Args { - #[clap(value_parser)] - input_left: PathBuf, - - #[clap(value_parser)] - input_right: PathBuf, - + /// Path to the description #[clap(short, long, value_parser)] desc: PathBuf, @@ -126,13 +200,14 @@ struct Args { #[clap(short, long, value_parser, default_value = "false")] no_resize: bool, - #[clap(long, value_parser)] - name: String, - /// Dimension to export the images as; can be specified multiple times #[clap(long, value_parser)] dim: Vec, + /// Input directory, containing the svgs to combine + #[clap(short, long, value_parser)] + input_dir: Option, + /// Output directory #[clap(short, long, value_parser)] output_dir: Option, diff --git a/src/parse.rs b/src/parse.rs index a4aef5c..f744d6a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -126,3 +126,18 @@ fn read_dir_xml(path: impl AsRef) -> HashMap { res } + +pub fn parse_css<'b>(css: &'b str) -> impl Iterator + 'b { + css.split(';').filter_map(|rule| { + let mut iter = rule.splitn(2, ':'); + if let Some(name) = iter.next() { + if let Some(value) = iter.next() { + Some((name.trim(), value)) + } else { + None + } + } else { + None + } + }) +} diff --git a/src/template.rs b/src/template.rs index d71876f..8b121a8 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,4 +1,4 @@ -use crate::parse::SpeciesDecl; +use crate::parse::{SpeciesDecl, parse_css}; use mustache::{Context, Data, MapBuilder, PartialLoader, Template}; use std::collections::HashMap; use std::path::Path; @@ -256,11 +256,9 @@ macro_rules! set_color { if let Some(style) = xml.attributes.get_mut("style") { let mut new_style = Vec::new(); - for rule in style.split(';') { - if let [name, value] = rule.splitn(2, ':').collect::>()[..] { - if name.trim() != $color_name && name.trim() != $opacity_name { - new_style.push(format!("{}:{}", name, value)); - } + for (name, value) in parse_css(style) { + if name != $color_name && name != $opacity_name { + new_style.push(format!("{}:{}", name, value)); } }