Working snuggle generator

main
Shad Amethyst 2 years ago
parent 1cd8665df7
commit 06a2fa6268
Signed by untrusted user: amethyst
GPG Key ID: D970C8DD1D6DEE36

@ -1,4 +1,31 @@
name = "snuggle" name = "snuggle"
dx = -60 dx = -90
dy = -40 dy = -30
transform = "scale(1.02 1.02) translate(-1.5 -1)" 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"

@ -14,6 +14,7 @@ base = ["body-basic", "eyes-basic", "mouth-w"]
happy = ["body-basic", "eyes-happy", "mouth-w"] happy = ["body-basic", "eyes-happy", "mouth-w"]
evil = ["body-basic", "eyes-evil", "mouth-w"] evil = ["body-basic", "eyes-evil", "mouth-w"]
owo = ["body-basic", "ear-owo", "eyes-owo", "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" = ["body-basic", "eyes-basic", "mouth-w", "hand-3c", "left-hand"]
"3c_evil" = ["body-basic", "eyes-evil", "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"] heart_pan = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding", "big-object"]
# Snuggle # Snuggle
snuggle_left = ["body-basic", "eyes-closed", "mouth-w"]
snuggle_right = ["body-snuggle", "eyes-snuggle", "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"]

@ -8,6 +8,9 @@
{{#tags.eyes-aww}} {{#tags.eyes-aww}}
{{#reach_aww}}#nose-outline{{/reach_aww}} {{#reach_aww}}#nose-outline{{/reach_aww}}
{{/tags.eyes-aww}} {{/tags.eyes-aww}}
{{#tags.eyes-happy}}
{{#snug}}#nose-outline{{/snug}}
{{/tags.eyes-happy}}
{{#tags.eyes-evil}} {{#tags.eyes-evil}}
{{#3c_evil}}#nose-outline{{/3c_evil}} {{#3c_evil}}#nose-outline{{/3c_evil}}
{{/tags.eyes-evil}} {{/tags.eyes-evil}}

@ -0,0 +1,7 @@
{{>header}}
{{>body}}
{{>eyes}}
{{>nose}}
{{>mouth}}
{{>footer}}

@ -0,0 +1,7 @@
{{>header}}
{{>body}}
{{>eyes}}
{{>nose}}
{{>mouth}}
{{>footer}}

@ -0,0 +1,10 @@
{{>header}}
{{>body}}
<g transform="rotate(-10 45 75)">
{{>eyes}}
{{>nose}}
{{>mouth}}
{{>blush}}
</g>
{{>footer}}

@ -2,6 +2,7 @@
use clap::Parser; use clap::Parser;
use std::fmt::Write; use std::fmt::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::collections::HashMap;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use xmltree::{Element, XMLNode}; use xmltree::{Element, XMLNode};
@ -13,48 +14,82 @@ use blobfox_template::{
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
struct Desc { struct Desc {
/// Name of the snuggle emote (eg. `snuggle`, `nom`)
name: String,
/// How much to move the "left" emote by, horizontally
dx: f64, dx: f64,
/// How much to move the "left" emote by, vertically
dy: f64, dy: f64,
/// How much to scale the "left" emote by, unimplemented!
scale: Option<f64>, scale: Option<f64>,
/// 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)] #[serde(default)]
transform: String, transform: String,
/// name/filename list of emotes for the "left" emotes
left: HashMap<String, String>,
/// name/filename list of emotes for the "right" emotes
right: HashMap<String, String>,
} }
fn main() { fn main() {
let args = Args::parse(); 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| { let files = std::fs::read_dir(&input_dir).unwrap_or_else(|err| {
panic!("Couldn't open {}: {}", args.input_right.display(), err); panic!("Couldn't read directory {}: {}", input_dir.display(), err);
}); }).filter_map(|entry| {
let right = std::fs::read_to_string(&args.input_right).unwrap_or_else(|err| { let entry = entry.ok()?;
panic!("Couldn't open {}: {}", args.input_right.display(), err); Some((entry.path().file_stem()?.to_str()?.to_string(), entry.path()))
}); }).collect::<HashMap<_, _>>();
let desc = std::fs::read_to_string(&args.desc).unwrap_or_else(|err| { let desc = std::fs::read_to_string(&args.desc).unwrap_or_else(|err| {
panic!("Couldn't open {}: {}", args.desc.display(), err); panic!("Couldn't open {}: {}", args.desc.display(), err);
}); });
let desc: Desc = toml::from_str(&desc).unwrap(); let desc: Desc = toml::from_str(&desc).unwrap();
let snuggle = generate_snuggle(left, right, desc); let export_args: export::ExportArgs = args.clone().into();
let snuggle = export::xml_to_str(&snuggle).unwrap();
for (left_name, left_path) in desc.left.iter() {
let output_dir = args.output_dir.clone().unwrap_or(PathBuf::from("output/")); if let Some(left_path) = files.get(left_path) {
let left = std::fs::read_to_string(left_path).unwrap_or_else(|err| {
export::export( panic!("Couldn't open {}: {}", left_path.display(), err);
snuggle, });
&output_dir,
args.name.clone(), for (right_name, right_path) in desc.right.iter() {
&args.clone().into() if let Some(right_path) = files.get(right_path) {
).unwrap(); 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_usvg = export::get_usvg(&left).unwrap();
let left_bbox = left_usvg.svg_node().view_box.rect; let left_bbox = left_usvg.svg_node().view_box.rect;
// // == Generate mask ==
let mut mask = Element::new("mask"); let mut mask = Element::new("mask");
mask.attributes.insert("id".to_string(), "snuggle-mask".to_string()); 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)); mask.children.push(XMLNode::Element(rect));
let mut right_mask = Element::new("g"); 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(); 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_fill("#000000", &mut right_xml);
template::set_stroke("#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)); mask.children.push(XMLNode::Element(right_mask));
// == Insert both emotes ==
let mut right_xml = Element::parse(right.as_bytes()).unwrap(); let mut right_xml = Element::parse(right.as_bytes()).unwrap();
let left_xml = Element::parse(left.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.attributes.insert("mask".to_string(), "url(#snuggle-mask)".to_string());
left_group2.children.push(XMLNode::Element(left_group)); left_group2.children.push(XMLNode::Element(left_group));
// == Fill in root element ==
let mut res = Element::new("svg"); let mut res = Element::new("svg");
res.attributes.insert("xmlns".to_string(), "http://www.w3.org/2000/svg".to_string()); res.attributes.insert("xmlns".to_string(), "http://www.w3.org/2000/svg".to_string());
res.attributes.insert("version".to_string(), "1.1".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 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::<f64>() {
*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::<f64>().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)] #[derive(Parser, Clone)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about, long_about = None)]
struct Args { struct Args {
#[clap(value_parser)] /// Path to the description
input_left: PathBuf,
#[clap(value_parser)]
input_right: PathBuf,
#[clap(short, long, value_parser)] #[clap(short, long, value_parser)]
desc: PathBuf, desc: PathBuf,
@ -126,13 +200,14 @@ struct Args {
#[clap(short, long, value_parser, default_value = "false")] #[clap(short, long, value_parser, default_value = "false")]
no_resize: bool, no_resize: bool,
#[clap(long, value_parser)]
name: String,
/// Dimension to export the images as; can be specified multiple times /// Dimension to export the images as; can be specified multiple times
#[clap(long, value_parser)] #[clap(long, value_parser)]
dim: Vec<u32>, dim: Vec<u32>,
/// Input directory, containing the svgs to combine
#[clap(short, long, value_parser)]
input_dir: Option<PathBuf>,
/// Output directory /// Output directory
#[clap(short, long, value_parser)] #[clap(short, long, value_parser)]
output_dir: Option<PathBuf>, output_dir: Option<PathBuf>,

@ -126,3 +126,18 @@ fn read_dir_xml(path: impl AsRef<Path>) -> HashMap<String, PathBuf> {
res res
} }
pub fn parse_css<'b>(css: &'b str) -> impl Iterator<Item=(&'b str, &'b str)> + '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
}
})
}

@ -1,4 +1,4 @@
use crate::parse::SpeciesDecl; use crate::parse::{SpeciesDecl, parse_css};
use mustache::{Context, Data, MapBuilder, PartialLoader, Template}; use mustache::{Context, Data, MapBuilder, PartialLoader, Template};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@ -256,11 +256,9 @@ macro_rules! set_color {
if let Some(style) = xml.attributes.get_mut("style") { if let Some(style) = xml.attributes.get_mut("style") {
let mut new_style = Vec::new(); let mut new_style = Vec::new();
for rule in style.split(';') { for (name, value) in parse_css(style) {
if let [name, value] = rule.splitn(2, ':').collect::<Vec<_>>()[..] { if name != $color_name && name != $opacity_name {
if name.trim() != $color_name && name.trim() != $opacity_name { new_style.push(format!("{}:{}", name, value));
new_style.push(format!("{}:{}", name, value));
}
} }
} }

Loading…
Cancel
Save