~fgaz/blobfox

06a2fa6268bd2b6f13e23a6d7b250bda4ce7780a — Adrien Burgun 1 year, 6 months ago 1cd8665
:sparkles: Working snuggle generator
M snuggle.toml => snuggle.toml +30 -3
@@ 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"

M species/blobfox/species.toml => species/blobfox/species.toml +3 -1
@@ 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"]

M species/blobfox/templates/nose.mustache => species/blobfox/templates/nose.mustache +3 -0
@@ 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}}

A species/blobfox/variants/aww.mustache => species/blobfox/variants/aww.mustache +7 -0
@@ 0,0 1,7 @@
{{>header}}
    {{>body}}

    {{>eyes}}
    {{>nose}}
    {{>mouth}}
{{>footer}}

A species/blobfox/variants/snuggle_left.mustache => species/blobfox/variants/snuggle_left.mustache +7 -0
@@ 0,0 1,7 @@
{{>header}}
    {{>body}}

    {{>eyes}}
    {{>nose}}
    {{>mouth}}
{{>footer}}

A species/blobfox/variants/snuggle_right_blush.mustache => species/blobfox/variants/snuggle_right_blush.mustache +10 -0
@@ 0,0 1,10 @@
{{>header}}
    {{>body}}

    <g transform="rotate(-10 45 75)">
        {{>eyes}}
        {{>nose}}
        {{>mouth}}
        {{>blush}}
    </g>
{{>footer}}

M src/bin/snuggle.rs => src/bin/snuggle.rs +105 -30
@@ 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<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)]
    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() {
    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::<HashMap<_, _>>();

    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::<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)]
#[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<u32>,

    /// Input directory, containing the svgs to combine
    #[clap(short, long, value_parser)]
    input_dir: Option<PathBuf>,

    /// Output directory
    #[clap(short, long, value_parser)]
    output_dir: Option<PathBuf>,

M src/parse.rs => src/parse.rs +15 -0
@@ 126,3 126,18 @@ fn read_dir_xml(path: impl AsRef<Path>) -> HashMap<String, PathBuf> {

    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
        }
    })
}

M src/template.rs => src/template.rs +4 -6
@@ 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::<Vec<_>>()[..] {
                            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));
                        }
                    }