🔥 Snuggle generator [WIP]

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

1
.gitignore vendored

@ -2,3 +2,4 @@ original/
output/ output/
Cargo.lock Cargo.lock
target/ target/
blobfox-*.zip

@ -20,3 +20,4 @@ resvg = "0.23"
usvg = "0.23" usvg = "0.23"
tiny-skia = "0.6" tiny-skia = "0.6"
png = "0.17" png = "0.17"
css-color-parser = "0.1.2"

@ -41,9 +41,21 @@ If you'd like to help, there are a few things that need attention outside of imp
- german shepherd - german shepherd
- collie - collie
- sheep - sheep
- bird
- etc. - etc.
- clean up the SVG for the existing emotes (the `clean` binary in `feat/template` is meant to do the heavy-lifting) - clean up the SVG for the existing emotes (the `clean` binary in `feat/template` is meant to do the heavy-lifting)
### TODO
- set_stroke!
- blobfox_ohmy
- blobfox_trumpet
- blobfox_highfive
- googly eyes?
- tea/coffee
- sad
- uwu
## License ## License
All the code, images and assets of this repository are made available under the Apache 2.0 license. All the code, images and assets of this repository are made available under the Apache 2.0 license.

@ -0,0 +1,4 @@
name = "snuggle"
dx = -60
dy = -40
transform = "scale(1.02 1.02) translate(-1.5 -1)"

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" height="128" width="128" sodipodi:docname="blobfox_snuggle_right.svg" id="svg5" inkscape:version="1.1" viewBox="0 0 128 128">
<title id="title30762">blobfox</title>
<sodipodi:namedview id="namedview7" borderopacity="1" bordercolor="#ffffff" inkscape:window-height="779" inkscape:zoom="9.7841572" inkscape:pageopacity="0" inkscape:showpageshadow="2" inkscape:current-layer="g9491" units="px" inkscape:document-units="px" inkscape:window-maximized="1" inkscape:window-x="0" pagecolor="#505050" inkscape:pagecheckerboard="1" showgrid="false" inkscape:window-width="1536" inkscape:window-y="0" inkscape:cy="46.401544" inkscape:pageshadow="0" inkscape:deskcolor="#505050" inkscape:cx="59.68833">
<inkscape:grid type="xygrid" id="grid10"/>
</sodipodi:namedview>
<defs id="defs2"/>
<g style="display:inline" inkscape:label="ref" inkscape:groupmode="layer" id="ref">
<image id="blobfox" inkscape:label="blobfox" x="0" preserveAspectRatio="none" y="0" width="127.99999" xlink:href="../original/blobfox.png" style="display:none;fill:#313131;fill-opacity:1;stroke:none;image-rendering:optimizeQuality" height="127.99999"/>
<image id="blobfoxsnuggle" xlink:href="../original/blobfoxsnuggle.png" y="-106.29223" style="image-rendering:optimizeSpeed" preserveAspectRatio="none" inkscape:label="blobfoxsnuggle" height="221.0103" width="221.0103" x="-93.369904"/>
</g>
<g id="Base" inkscape:label="Base" inkscape:groupmode="layer" style="display:inline">
<path d="M 12.87231,40.818966 C -2.8719971,22.749014 -4.3550533,3.1367661 -2.9535978,0.24374984 -1.8493312,-2.0357805 -0.73468479,-3.6569086 1.9073133,-2.7558898 5.0260913,-1.6922712 17.794722,2.5508374 34.540893,8.7120265 c 7.146252,2.6292225 3.161601,9.4167855 1.87439,12.1261315 -2.856028,5.37387 -17.877727,10.71439 -23.542973,19.980808 z" id="left-ear" sodipodi:nodetypes="cssscc" style="display:inline;fill:#313131;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" inkscape:label="left-ear"/>
<path d="m 23.769283,6.3584073 c 2.991294,4.2455057 4.569848,7.4475027 8.895201,10.9888377 -12.070861,-0.541455 -15.260708,1.69375 -19.417541,5.559159 7.973832,0.09289 11.531043,0.522786 15.204226,1.155863 12.215874,2.105419 38.518977,5.035068 47.371493,-2.486915 4.32997,-3.679173 3.410201,-7.265503 -2.316952,-10.378575 C 67.610927,7.9925869 55.645095,4.8584742 51.222049,3.8438904 52.004054,6.0173862 53.35612,8.7380188 54.213344,11.268754 45.9815,7.6883395 37.893549,5.4027147 23.769283,6.3584073 Z" style="display:inline;fill:#ff8702;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.4501" sodipodi:nodetypes="cccsssccc" id="hair" inkscape:label="hair"/>
<path style="display:inline;fill:#ff8702;fill-opacity:1;stroke:none;stroke-width:2.64567;stroke-linecap:square;stroke-miterlimit:3" inkscape:label="body" d="m 6.0599734,91.649344 c 0,-8.73002 2.525321,-5.814368 2.6883167,-12.346173 C 8.9112858,72.771365 3.4640017,66.503444 3.0623888,53.796109 2.608823,39.444946 14.618107,15.642023 46.64457,15.642023 c 33.541601,0 57.93714,1.121349 70.19416,23.200939 12.45071,22.428499 12.91096,52.067554 6.38429,59.723645 -6.52667,7.656093 -22.73142,15.897513 -56.529275,15.897513 -33.797859,0 -60.6337716,-12.74755 -60.6337716,-22.814776 z" id="body" sodipodi:nodetypes="cssssssc"/>
<path style="display:inline;fill:#313131;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.4501" sodipodi:nodetypes="scscccss" d="m 109.65875,-11.393576 c -9.0534,1.6065407 -29.38453,9.7496393 -44.524796,30.678621 0.210609,4.044387 7.117027,6.692013 15.231023,5.036186 8.412314,-1.716705 21.594053,-22.0009765 26.970613,-27.4823623 1.00114,9.5497778 -12.65173,31.3913833 -4.57278,40.2752173 3.27823,3.604628 6.48404,-4.729898 7.41016,-7.69531 3.7465,-12.049663 6.22482,-21.0511645 6.3764,-27.8592275 0.0598,-2.686778 1.92218,-14.5169695 -6.89062,-12.9531245 z" inkscape:label="right-ear" id="right-ear"/>
<path id="right-ear-fluff" style="display:inline;fill:#ebdccc;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" inkscape:label="right-ear-fluff" d="M 109.7442,-7.9162135 C 101.79383,-4.5766893 95.432201,2.858004 93.256387,11.957451 92.338149,15.7976 86.384544,21.344383 83.242552,23.116446 c 3.859188,-0.486555 8.080586,-0.662358 9.623136,-0.582576 -0.593528,2.540618 -1.150986,5.084216 -1.270641,7.589149 2.095641,-2.400784 6.129327,-4.663902 8.596323,-5.42458 -0.870477,4.814098 -0.218062,7.843034 1.18668,10.437489 0.3855,-11.747093 7.77479,-14.345491 10.05823,-21.014332 1.71162,-4.9987961 4.75466,-15.8385462 1.25678,-21.4073931 -0.64101,-1.0205338 -2.00455,-1.0270711 -2.94886,-0.6304164 z" sodipodi:nodetypes="sscccccsss"/>
</g>
<g id="Features" inkscape:label="Features" transform="rotate(-10 45 75)" style="display:inline" inkscape:groupmode="layer">
<path id="nose-outline" d="m 33.65228,80.15316 c -1.84571,-3.806604 -1.872556,-7.54518 0.527665,-10.056125 2.400221,-2.510944 6.697437,-10.228997 6.681819,-14.101173" inkscape:label="nose-outline" sodipodi:nodetypes="csc" style="fill:none;fill-opacity:1;stroke:#313131;stroke-width:4.40315;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path inkscape:label="nose" d="m 40.349664,72.473149 c 3.611758,-1.970317 14.548618,-2.669295 17.071165,2.5758 0.40334,0.838655 -6.115736,7.143991 -10.106864,7.135136 -3.859049,-0.0087 -7.870186,-9.216748 -6.964301,-9.710936 z" style="fill:#313131;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" id="nose" sodipodi:nodetypes="ssss"/>
<path id="mouth" d="m 30.329986,85.466434 c 2.073506,6.374978 4.020579,10.401679 5.959243,12.940501 3.750744,-3.131043 8.538975,-7.475595 10.436736,-9.373813 2.962201,4.818455 7.017346,10.375135 8.671615,12.645698 4.859078,-3.873207 9.518861,-7.995561 11.43575,-10.162866" style="fill:none;stroke:#313131;stroke-width:4.40315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" inkscape:label="mouth" sodipodi:nodetypes="ccccc"/>
<path d="m 24.641601,45.918665 c -4.075919,0.730189 -6.501377,3.925039 -6.273966,5.603347 0.227411,1.678308 1.347515,2.224702 3.8872,1.666965 2.397981,-0.52641 9.098771,-2.404689 17.303469,3.65651 2.148157,1.586937 4.650972,-0.810343 2.741314,-2.795355 -7.65445,-7.956509 -12.747952,-9.01107 -17.658017,-8.131467 z" sodipodi:nodetypes="ssssss" inkscape:label="left-eye" style="display:inline;fill:#313131;fill-opacity:1;stroke-width:4.45223;stroke-linecap:round" id="left-eye"/>
<path id="right-eye" inkscape:label="right-eye" sodipodi:nodetypes="ssssss" style="display:inline;fill:#313131;fill-opacity:1;stroke-width:4.41528;stroke-linecap:round" d="m 83.448042,49.242516 c 3.928913,1.194349 5.945041,4.623512 5.525393,6.249833 -0.419635,1.626313 -1.586453,2.033818 -4.022559,1.188423 -2.300205,-0.798011 -8.680288,-3.429088 -17.467185,1.58346 -2.300578,1.312383 -4.485891,-1.340335 -2.373777,-3.072561 8.46599,-6.943249 13.605145,-7.38791 18.338128,-5.949155 z"/>
</g>
<metadata id="metadata30760">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:rights>
<cc:Agent>
<dc:title>Blobfox team (https://git.shadamethyst.xyz/adri326/blobfox), licensed under the Apache 2.0 License</dc:title>
</cc:Agent>
</dc:rights>
<dc:title>blobfox</dc:title>
<dc:creator>
<cc:Agent>
<dc:title>Feuerfuchs</dc:title>
</cc:Agent>
</dc:creator>
<dc:source>https://git.shadamethyst.xyz/adri326/blobfox</dc:source>
<dc:contributor>
<cc:Agent>
<dc:title>Shad Amethyst</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

@ -6,6 +6,7 @@ body_color = "#ff8702"
ear_color = "#313131" ear_color = "#313131"
ear_fluff_color = "#ebdccc" ear_fluff_color = "#ebdccc"
hand_color = "#ff8702" hand_color = "#ff8702"
hand_stroke_color = "#313131"
tail_color = "#ff8702" tail_color = "#ff8702"
[variants] [variants]
@ -51,3 +52,7 @@ heart_enby = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding",
heart_ace = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding", "big-object"] heart_ace = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding", "big-object"]
heart_demisexual = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding", "big-object"] heart_demisexual = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding", "big-object"]
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_right = ["body-snuggle", "eyes-snuggle", "mouth-w"]
snuggle_right_shadow = ["body-snuggle", "eyes-snuggle", "mouth-w"]

@ -0,0 +1,15 @@
{{! Left ear }}
{{#set-fill}} {{vars.ear_color}} | {{#snuggle_right}}#left-ear{{/snuggle_right}} {{/set-fill}}
{{! Body }}
<defs>
<clipPath id="body-clip">
{{#snuggle_right}}#body{{/snuggle_right}}
</clipPath>
</defs>
{{#set-fill}} {{vars.body_color}} | {{#snuggle_right}}#body{{/snuggle_right}} {{/set-fill}}
{{#set-fill}} {{vars.body_color}} | {{#snuggle_right}}#hair{{/snuggle_right}} {{/set-fill}}
{{! Right ear }}
{{#set-fill}} {{vars.ear_color}} | {{#snuggle_right}}#right-ear{{/snuggle_right}} {{/set-fill}}
{{#set-fill}} {{vars.ear_fluff_color}} | {{#snuggle_right}}#right-ear-fluff{{/snuggle_right}} {{/set-fill}}

@ -8,4 +8,7 @@
{{#tags.body-comfy}} {{#tags.body-comfy}}
{{>body-comfy}} {{>body-comfy}}
{{/tags.body-comfy}} {{/tags.body-comfy}}
{{#tags.body-snuggle}}
{{>body-snuggle}}
{{/tags.body-snuggle}}
</g> </g>

@ -23,4 +23,8 @@
{{#blush}}#left-eye{{/blush}} {{#blush}}#left-eye{{/blush}}
{{#blush}}#right-eye{{/blush}} {{#blush}}#right-eye{{/blush}}
{{/tags.eyes-closed}} {{/tags.eyes-closed}}
{{#tags.eyes-snuggle}}
{{#snuggle_right}}#left-eye{{/snuggle_right}}
{{#snuggle_right}}#right-eye{{/snuggle_right}}
{{/tags.eyes-snuggle}}
</g> </g>

@ -1,7 +1,19 @@
<g id="hands"> <g id="hands">
{{#tags.hands-reach}} {{#tags.hands-reach}}
{{#set-fill}} {{vars.hand_color}} | {{#reach_aww}}#left-hand{{/reach_aww}} {{/set-fill}} {{#set-stroke}}
{{#set-fill}} {{vars.hand_color}} | {{#reach_aww}}#right-hand{{/reach_aww}} {{/set-fill}} {{vars.hand_stroke_color}} |
{{#set-fill}}
{{vars.hand_color}}
| {{#reach_aww}}#left-hand{{/reach_aww}}
{{/set-fill}}
{{/set-stroke}}
{{#set-stroke}}
{{vars.hand_stroke_color}} |
{{#set-fill}}
{{vars.hand_color}}
| {{#reach_aww}}#right-hand{{/reach_aww}}
{{/set-fill}}
{{/set-stroke}}
{{/tags.hands-reach}} {{/tags.hands-reach}}
{{#tags.hand-3c}} {{#tags.hand-3c}}
{{#tags.holding}} {{#tags.holding}}

@ -14,6 +14,9 @@
{{#tags.eyes-closed}} {{#tags.eyes-closed}}
{{#blush}}#nose-outline{{/blush}} {{#blush}}#nose-outline{{/blush}}
{{/tags.eyes-closed}} {{/tags.eyes-closed}}
{{#tags.eyes-snuggle}}
{{#snuggle_right}}#nose-outline{{/snuggle_right}}
{{/tags.eyes-snuggle}}
{{#base}}#nose{{/base}} {{#base}}#nose{{/base}}
</g> </g>

@ -9,7 +9,11 @@
</clipPath> </clipPath>
</defs> </defs>
<g clip-path="url(#clip-heart)"> <g blobfox-only-size="true">
{{#heart}}#heart{{/heart}}
</g>
<g clip-path="url(#clip-heart)" blobfox-ignore-size="true">
<g transform="rotate(9) scale(1.05 1.05) translate(10 45)"> <g transform="rotate(9) scale(1.05 1.05) translate(10 45)">
{{#flag_ace}}{{/flag_ace}} {{#flag_ace}}{{/flag_ace}}
</g> </g>

@ -9,7 +9,11 @@
</clipPath> </clipPath>
</defs> </defs>
<g clip-path="url(#clip-heart)"> <g blobfox-only-size="true">
{{#heart}}#heart{{/heart}}
</g>
<g clip-path="url(#clip-heart)" blobfox-ignore-size="true">
<g transform="rotate(9) scale(1.05 1.05) translate(10 45)"> <g transform="rotate(9) scale(1.05 1.05) translate(10 45)">
{{#flag_demisexual}}{{/flag_demisexual}} {{#flag_demisexual}}{{/flag_demisexual}}
</g> </g>

@ -9,7 +9,11 @@
</clipPath> </clipPath>
</defs> </defs>
<g clip-path="url(#clip-heart)"> <g blobfox-only-size="true">
{{#heart}}#heart{{/heart}}
</g>
<g clip-path="url(#clip-heart)" blobfox-ignore-size="true">
<g transform="rotate(9) scale(1.05 1.05) translate(10 45)"> <g transform="rotate(9) scale(1.05 1.05) translate(10 45)">
{{#flag_enby}}{{/flag_enby}} {{#flag_enby}}{{/flag_enby}}
</g> </g>

@ -9,7 +9,11 @@
</clipPath> </clipPath>
</defs> </defs>
<g clip-path="url(#clip-heart)"> <g blobfox-only-size="true">
{{#heart}}#heart{{/heart}}
</g>
<g clip-path="url(#clip-heart)" blobfox-ignore-size="true">
<g transform="rotate(9) scale(1.05 1.05) translate(10 45)"> <g transform="rotate(9) scale(1.05 1.05) translate(10 45)">
{{#flag_pan}}{{/flag_pan}} {{#flag_pan}}{{/flag_pan}}
</g> </g>

@ -9,7 +9,11 @@
</clipPath> </clipPath>
</defs> </defs>
<g clip-path="url(#clip-heart)"> <g blobfox-only-size="true">
{{#heart}}#heart{{/heart}}
</g>
<g clip-path="url(#clip-heart)" blobfox-ignore-size="true">
<g transform="rotate(9) scale(0.95 0.95) translate(20 50)"> <g transform="rotate(9) scale(0.95 0.95) translate(20 50)">
{{#flag_progress}}{{/flag_progress}} {{#flag_progress}}{{/flag_progress}}
</g> </g>

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

@ -0,0 +1,148 @@
//! Very crude tool for generating snuggle emotes
use clap::Parser;
use std::fmt::Write;
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use xmltree::{Element, XMLNode};
use blobfox_template::{
parse,
template,
export,
};
#[derive(Serialize, Deserialize, Debug)]
struct Desc {
dx: f64,
dy: f64,
scale: Option<f64>,
#[serde(default)]
transform: String,
}
fn main() {
let args = Args::parse();
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 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();
}
fn generate_snuggle(left: String, right: String, desc: Desc) -> Element {
let left_usvg = export::get_usvg(&left).unwrap();
let left_bbox = left_usvg.svg_node().view_box.rect;
//
let mut mask = Element::new("mask");
mask.attributes.insert("id".to_string(), "snuggle-mask".to_string());
let mut rect = Element::new("rect");
rect.attributes.insert("fill".to_string(), "white".to_string());
// TODO: use scale?
rect.attributes.insert("x".to_string(), (desc.dx + left_bbox.x()).to_string());
rect.attributes.insert("y".to_string(), (desc.dy + left_bbox.y()).to_string());
rect.attributes.insert("width".to_string(), left_bbox.width().to_string());
rect.attributes.insert("height".to_string(), left_bbox.height().to_string());
mask.children.push(XMLNode::Element(rect));
let mut right_mask = Element::new("g");
right_mask.attributes.insert("transform".to_string(), desc.transform);
let mut right_xml = Element::parse(right.as_bytes()).unwrap();
template::set_fill("#000000", &mut right_xml);
template::set_stroke("#000000", &mut right_xml);
for child in right_xml.children {
if let XMLNode::Element(child) = child {
right_mask.children.push(XMLNode::Element(child));
}
}
mask.children.push(XMLNode::Element(right_mask));
let mut right_xml = Element::parse(right.as_bytes()).unwrap();
let left_xml = Element::parse(left.as_bytes()).unwrap();
let mut left_group = Element::new("g");
left_group.attributes.insert("transform".to_string(), format!(
"translate({} {})",
desc.dx,
desc.dy
));
left_group.children = left_xml.children;
let mut left_group2 = Element::new("g");
left_group2.attributes.insert("mask".to_string(), "url(#snuggle-mask)".to_string());
left_group2.children.push(XMLNode::Element(left_group));
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());
res.attributes.insert("width".to_string(), "128".to_string());
res.attributes.insert("height".to_string(), "128".to_string());
res.children.push(XMLNode::Element(mask));
res.children.append(&mut right_xml.children);
res.children.push(XMLNode::Element(left_group2));
res
}
#[derive(Parser, Clone)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(value_parser)]
input_left: PathBuf,
#[clap(value_parser)]
input_right: PathBuf,
#[clap(short, long, value_parser)]
desc: PathBuf,
/// Disable automatically resizing the SVG's viewBox, defaults to false
#[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>,
/// Output directory
#[clap(short, long, value_parser)]
output_dir: Option<PathBuf>,
}
impl From<Args> for export::ExportArgs {
fn from(args: Args) -> export::ExportArgs {
export::ExportArgs {
no_resize: args.no_resize,
dim: args.dim,
}
}
}

@ -48,6 +48,28 @@ impl From<png::EncodingError> for ExportError {
} }
} }
pub struct ExportArgs {
pub no_resize: bool,
pub dim: Vec<u32>,
}
pub fn get_usvg(svg_str: &str) -> Result<usvg::Tree, usvg::Error> {
let usvg_options = Options::default();
Tree::from_str(svg_str, &usvg_options.to_ref())
}
pub fn get_xml(svg_str: &str) -> Result<Element, xmltree::ParseError> {
Element::parse(svg_str.as_bytes())
}
pub fn xml_to_str(svg_xml: &Element) -> Result<String, ExportError> {
let mut s: Vec<u8> = Vec::new();
svg_xml.write(&mut s)?;
Ok(String::from_utf8(s)?)
}
fn get_new_bbox(svg: &Tree) -> Option<(f64, f64, f64, f64)> { fn get_new_bbox(svg: &Tree) -> Option<(f64, f64, f64, f64)> {
let bbox = svg.root().calculate_bbox()?; let bbox = svg.root().calculate_bbox()?;
@ -70,25 +92,45 @@ fn get_new_bbox(svg: &Tree) -> Option<(f64, f64, f64, f64)> {
} }
} }
fn get_usvg(svg_str: &str) -> Result<usvg::Tree, usvg::Error> { /// Removes all elements marked with `blobfox-ignore-size="true"`
let usvg_options = Options::default();
Tree::from_str(svg_str, &usvg_options.to_ref())
}
fn get_xml(svg_str: &str) -> Result<Element, xmltree::ParseError> { macro_rules! strip {
Element::parse(svg_str.as_bytes()) ( $name:tt, $attribute:expr ) => {
} fn $name(svg_str: &str) -> Result<String, ExportError> {
let mut xml = get_xml(svg_str)?;
fn xml_to_str(svg_xml: &Element) -> Result<String, ExportError> { fn rec(element: &mut Element) {
let mut s: Vec<u8> = Vec::new(); // TODO: replace with Vec::drain_filter once https://github.com/rust-lang/rust/issues/43244 gets merged
for child in std::mem::take(&mut element.children) {
match child {
XMLNode::Element(mut child) => {
if let Some("true") = child.attributes.get($attribute).map(|s| s.as_str()) {
continue
}
svg_xml.write(&mut s)?; rec(&mut child);
Ok(String::from_utf8(s)?) element.children.push(XMLNode::Element(child));
}
child => element.children.push(child),
}
}
}
rec(&mut xml);
xml_to_str(&xml)
}
}
} }
strip!(strip_ignore_size, "blobfox-ignore-size");
strip!(strip_only_size, "blobfox-only-size");
pub fn resize(svg_str: String) -> Result<String, ExportError> { pub fn resize(svg_str: String) -> Result<String, ExportError> {
if let Some(new_bbox) = get_new_bbox(&get_usvg(&svg_str)?) { let stripped = strip_ignore_size(&svg_str)?;
if let Some(new_bbox) = get_new_bbox(&get_usvg(&stripped)?) {
let mut svg_xml = get_xml(&svg_str)?; let mut svg_xml = get_xml(&svg_str)?;
svg_xml.attributes.insert( svg_xml.attributes.insert(
"viewBox".to_string(), "viewBox".to_string(),
@ -144,12 +186,14 @@ pub fn export(
mut svg_str: String, mut svg_str: String,
output_dir: &PathBuf, output_dir: &PathBuf,
output_name: String, output_name: String,
args: &super::Args, args: &ExportArgs,
) -> Result<(), ExportError> { ) -> Result<(), ExportError> {
if !args.no_resize { if !args.no_resize {
svg_str = resize(svg_str)?; svg_str = resize(svg_str)?;
} }
svg_str = strip_only_size(&svg_str)?;
svg_str = combine_defs(svg_str)?; svg_str = combine_defs(svg_str)?;
mkdirp::mkdirp(output_dir.join("vector")).unwrap(); mkdirp::mkdirp(output_dir.join("vector")).unwrap();

@ -0,0 +1,3 @@
pub mod parse;
pub mod template;
pub mod export;

@ -1,14 +1,11 @@
use clap::Parser; use clap::Parser;
use std::path::PathBuf; use std::path::PathBuf;
pub mod parse; use blobfox_template::{
use parse::*; parse::*,
template::*,
pub mod template; export::*,
use template::*; };
pub mod export;
use export::*;
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
@ -30,6 +27,8 @@ fn main() {
} }
fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf, args: &Args) { fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf, args: &Args) {
let args: ExportArgs = args.clone().into();
if let Some(path) = context.species().variant_paths.get(name) { if let Some(path) = context.species().variant_paths.get(name) {
match context.compile(path).and_then(|template| { match context.compile(path).and_then(|template| {
template.render_data_to_string(&context.get_data(name)) template.render_data_to_string(&context.get_data(name))
@ -39,7 +38,7 @@ fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf
svg, svg,
output_dir, output_dir,
format!("{}_{}", context.species().name, name), format!("{}_{}", context.species().name, name),
args &args
) { ) {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
@ -56,7 +55,7 @@ fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf
} }
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about, long_about = None)]
pub struct Args { pub struct Args {
/// A folder containing the declaration from which the emotes should be generated /// A folder containing the declaration from which the emotes should be generated
@ -79,3 +78,12 @@ pub struct Args {
#[clap(short, long, value_parser)] #[clap(short, long, value_parser)]
output_dir: Option<PathBuf>, output_dir: Option<PathBuf>,
} }
impl From<Args> for ExportArgs {
fn from(args: Args) -> ExportArgs {
ExportArgs {
no_resize: args.no_resize,
dim: args.dim,
}
}
}

@ -1,9 +1,10 @@
use super::*; use crate::parse::SpeciesDecl;
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;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use xmltree::{Element, XMLNode}; use xmltree::{Element, XMLNode};
use css_color_parser::Color as CssColor;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RenderingContext { pub struct RenderingContext {
@ -90,39 +91,45 @@ impl RenderingContext {
}); });
} }
let this = self.clone(); for (cb, name) in [
let variant_name_owned = variant_name.to_string(); (set_fill as fn(&str, &mut Element), "set-fill"),
builder = builder.insert_fn("set-fill", move |input| { (set_stroke, "set-stroke")
// Parse `color|xml` ] {
if let [color, xml] = input.splitn(2, '|').collect::<Vec<_>>()[..] { let this = self.clone();
// Render `color` and `xml` let variant_name_owned = variant_name.to_string();
if let (Ok(color), Ok(xml)) = (
this.render_to_string(&color, &variant_name_owned), builder = builder.insert_fn(name, move |input| {
this.render_to_string(&xml, &variant_name_owned), // Parse `color|xml`
) { if let [color, xml] = input.splitn(2, '|').collect::<Vec<_>>()[..] {
// Convert `xml` to XML // Render `color` and `xml`
match Element::parse(xml.as_bytes()) { if let (Ok(color), Ok(xml)) = (
Ok(mut xml) => { this.render_to_string(&color, &variant_name_owned),
set_fill(&color.trim(), &mut xml); this.render_to_string(&xml, &variant_name_owned),
) {
// Render XML to string // Convert `xml` to XML
if let Some(res) = xml_to_string(xml) { match Element::parse(xml.as_bytes()) {
res Ok(mut xml) => {
} else { cb(&color.trim(), &mut xml);
String::from("<!-- Error in stringifying xml -->")
// Render XML to string
if let Some(res) = xml_to_string(xml) {
res
} else {
String::from("<!-- Error in stringifying xml -->")
}
}
Err(err) => {
format!("<!-- Error in parsing xml: {} -->", err)
} }
} }
Err(err) => { } else {
format!("<!-- Error in parsing xml: {} -->", err) String::from("<!-- Error in parsing color or element -->")
}
} }
} else { } else {
String::from("<!-- Error in parsing color or element -->") String::from("<!-- Invalid syntax: expected `color|xml` -->")
} }
} else { });
String::from("<!-- Invalid syntax: expected `color|xml` -->") }
}
});
builder = builder.insert("vars", &self.species.vars).unwrap(); builder = builder.insert("vars", &self.species.vars).unwrap();
@ -236,34 +243,54 @@ impl PartialLoader for RenderingContext {
} }
} }
fn set_fill(color: &str, xml: &mut Element) { macro_rules! set_color {
// Substitute the fill color; TODO: handle transparency for SVG 1.1 ( $fn_name:tt, $color_name:expr, $opacity_name:expr ) => {
if let Some(style) = xml.attributes.get_mut("style") { pub fn $fn_name(color: &str, xml: &mut Element) {
let mut new_style = Vec::new(); let (color, opacity) = if let Ok(parsed) = color.parse::<CssColor>() {
(format!("#{:02x}{:02x}{:02x}", parsed.r, parsed.g, parsed.b), parsed.a)
} else {
(color.to_string(), 1.0)
};
for rule in style.split(';') { fn rec(color: &str, opacity: f32, xml: &mut Element) {
if let [name, value] = rule.splitn(2, ':').collect::<Vec<_>>()[..] { if let Some(style) = xml.attributes.get_mut("style") {
if name.trim() != "fill" && name.trim() != "fill-opacity" { let mut new_style = Vec::new();
new_style.push(format!("{}:{}", name, value));
} 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));
}
}
}
new_style.push(format!("fill: {};", color)); new_style.push(format!(concat!($color_name, ": {};"), color));
new_style.push(format!(concat!($opacity_name, ": {};"), opacity));
*style = new_style.join(";"); *style = new_style.join(";");
} }
if let Some(_fill) = xml.attributes.get("fill") { if let Some(_fill) = xml.attributes.get($color_name) {
xml.attributes.insert("fill".to_string(), color.to_string()); xml.attributes.insert($color_name.to_string(), color.to_string());
} }
if let Some(_fill) = xml.attributes.get($opacity_name) {
xml.attributes.insert($opacity_name.to_string(), opacity.to_string());
}
for child in xml.children.iter_mut() {
if let XMLNode::Element(ref mut child) = child {
rec(color, opacity, child);
}
}
}
for child in xml.children.iter_mut() { rec(&color, opacity, xml)
if let XMLNode::Element(ref mut child) = child {
set_fill(color, child);
} }
} }
} }
set_color!(set_fill, "fill", "fill-opacity");
set_color!(set_stroke, "stroke", "stroke-opacity");
pub fn query_selector(svg: Element, pattern: &str) -> Option<Element> { pub fn query_selector(svg: Element, pattern: &str) -> Option<Element> {
if pattern == "" { if pattern == "" {
// NOTE: it looks like having a nested svg makes resvg unhappy // NOTE: it looks like having a nested svg makes resvg unhappy

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
viewBox="0 0 128 128"
width="128"
sodipodi:docname="blobfox_snuggle_right.svg"
id="svg5"
height="128"
version="1.1"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><title
id="title30762">blobfox</title><sodipodi:namedview
inkscape:window-maximized="1"
bordercolor="#ffffff"
inkscape:showpageshadow="2"
inkscape:window-height="779"
inkscape:pageopacity="0"
borderopacity="1"
inkscape:cy="46.401544"
inkscape:window-x="0"
id="namedview7"
pagecolor="#505050"
inkscape:pageshadow="0"
showgrid="false"
inkscape:zoom="9.7841572"
inkscape:window-width="1536"
inkscape:window-y="0"
inkscape:current-layer="g9491"
inkscape:cx="59.68833"
units="px"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
inkscape:deskcolor="#505050"><inkscape:grid
id="grid10"
type="xygrid" /></sodipodi:namedview><defs
id="defs2" /><g
style="display:inline"
id="layer2"
inkscape:groupmode="layer"
inkscape:label="ref"><image
inkscape:label="blobfox"
y="0"
height="127.99999"
width="127.99999"
style="display:none;fill:#313131;fill-opacity:1;stroke:none;image-rendering:optimizeQuality"
x="0"
preserveAspectRatio="none"
xlink:href="../original/blobfox.png"
id="image80" /><image
width="221.0103"
height="221.0103"
preserveAspectRatio="none"
style="image-rendering:optimizeSpeed"
xlink:href="../original/blobfoxsnuggle.png"
id="image545"
x="-93.369904"
y="-106.29223"
inkscape:label="blobfoxsnuggle" /></g><g
inkscape:label="Base"
id="layer1"
inkscape:groupmode="layer"
style="display:inline"><path
sodipodi:nodetypes="cssscc"
style="display:inline;fill:#313131;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 12.87231,40.818966 C -2.8719971,22.749014 -4.3550533,3.1367661 -2.9535978,0.24374984 -1.8493312,-2.0357805 -0.73468479,-3.6569086 1.9073133,-2.7558898 5.0260913,-1.6922712 17.794722,2.5508374 34.540893,8.7120265 c 7.146252,2.6292225 3.161601,9.4167855 1.87439,12.1261315 -2.856028,5.37387 -17.877727,10.71439 -23.542973,19.980808 z"
id="path20678"
inkscape:label="left-ear" /><path
id="path117-7"
inkscape:label="hair"
sodipodi:nodetypes="cccsssccc"
style="display:inline;fill:#ff8702;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.4501"
d="m 23.769283,6.3584073 c 2.991294,4.2455057 4.569848,7.4475027 8.895201,10.9888377 -12.070861,-0.541455 -15.260708,1.69375 -19.417541,5.559159 7.973832,0.09289 11.531043,0.522786 15.204226,1.155863 12.215874,2.105419 38.518977,5.035068 47.371493,-2.486915 4.32997,-3.679173 3.410201,-7.265503 -2.316952,-10.378575 C 67.610927,7.9925869 55.645095,4.8584742 51.222049,3.8438904 52.004054,6.0173862 53.35612,8.7380188 54.213344,11.268754 45.9815,7.6883395 37.893549,5.4027147 23.769283,6.3584073 Z" /><path
style="display:inline;fill:#ff8702;fill-opacity:1;stroke:none;stroke-width:2.64567;stroke-linecap:square;stroke-miterlimit:3"
d="m 6.0599734,91.649344 c 0,-8.73002 2.525321,-5.814368 2.6883167,-12.346173 C 8.9112858,72.771365 3.4640017,66.503444 3.0623888,53.796109 2.608823,39.444946 14.618107,15.642023 46.64457,15.642023 c 33.541601,0 57.93714,1.121349 70.19416,23.200939 12.45071,22.428499 12.91096,52.067554 6.38429,59.723645 -6.52667,7.656093 -22.73142,15.897513 -56.529275,15.897513 -33.797859,0 -60.6337716,-12.74755 -60.6337716,-22.814776 z"
id="path8285"
sodipodi:nodetypes="cssssssc"
inkscape:label="body" /><path
d="m 109.65875,-11.393576 c -9.0534,1.6065407 -29.38453,9.7496393 -44.524796,30.678621 0.210609,4.044387 7.117027,6.692013 15.231023,5.036186 8.412314,-1.716705 21.594053,-22.0009765 26.970613,-27.4823623 1.00114,9.5497778 -12.65173,31.3913833 -4.57278,40.2752173 3.27823,3.604628 6.48404,-4.729898 7.41016,-7.69531 3.7465,-12.049663 6.22482,-21.0511645 6.3764,-27.8592275 0.0598,-2.686778 1.92218,-14.5169695 -6.89062,-12.9531245 z"
id="path14428"
style="display:inline;fill:#313131;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.4501"
inkscape:label="right-ear"
sodipodi:nodetypes="scscccss" /><path
id="path18942"
style="display:inline;fill:#ebdccc;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
inkscape:label="right-ear-fluff"
d="M 109.7442,-7.9162135 C 101.79383,-4.5766893 95.432201,2.858004 93.256387,11.957451 92.338149,15.7976 86.384544,21.344383 83.242552,23.116446 c 3.859188,-0.486555 8.080586,-0.662358 9.623136,-0.582576 -0.593528,2.540618 -1.150986,5.084216 -1.270641,7.589149 2.095641,-2.400784 6.129327,-4.663902 8.596323,-5.42458 -0.870477,4.814098 -0.218062,7.843034 1.18668,10.437489 0.3855,-11.747093 7.77479,-14.345491 10.05823,-21.014332 1.71162,-4.9987961 4.75466,-15.8385462 1.25678,-21.4073931 -0.64101,-1.0205338 -2.00455,-1.0270711 -2.94886,-0.6304164 z"
sodipodi:nodetypes="sscccccsss" /></g><g
id="layer3"
inkscape:groupmode="layer"
inkscape:label="Features"
style="display:inline" transform="rotate(-10 45 75)"><path
d="m 33.65228,80.15316 c -1.84571,-3.806604 -1.872556,-7.54518 0.527665,-10.056125 2.400221,-2.510944 6.697437,-10.228997 6.681819,-14.101173"
sodipodi:nodetypes="csc"
inkscape:label="nose-outline"
id="path27175"
style="fill:none;fill-opacity:1;stroke:#313131;stroke-width:4.40315;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
id="path28517"
style="fill:#313131;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:nodetypes="ssss"
d="m 40.349664,72.473149 c 3.611758,-1.970317 14.548618,-2.669295 17.071165,2.5758 0.40334,0.838655 -6.115736,7.143991 -10.106864,7.135136 -3.859049,-0.0087 -7.870186,-9.216748 -6.964301,-9.710936 z"
inkscape:label="nose" /><path
inkscape:label="mouth"
id="path29369"
style="fill:none;stroke:#313131;stroke-width:4.40315;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 30.329986,85.466434 c 2.073506,6.374978 4.020579,10.401679 5.959243,12.940501 3.750744,-3.131043 8.538975,-7.475595 10.436736,-9.373813 2.962201,4.818455 7.017346,10.375135 8.671615,12.645698 4.859078,-3.873207 9.518861,-7.995561 11.43575,-10.162866"
sodipodi:nodetypes="ccccc" /><path
style="display:inline;fill:#313131;fill-opacity:1;stroke-width:4.45223;stroke-linecap:round"
sodipodi:nodetypes="ssssss"
d="m 24.641601,45.918665 c -4.075919,0.730189 -6.501377,3.925039 -6.273966,5.603347 0.227411,1.678308 1.347515,2.224702 3.8872,1.666965 2.397981,-0.52641 9.098771,-2.404689 17.303469,3.65651 2.148157,1.586937 4.650972,-0.810343 2.741314,-2.795355 -7.65445,-7.956509 -12.747952,-9.01107 -17.658017,-8.131467 z"
inkscape:label="left-eye"
id="left-eye" /><path
sodipodi:nodetypes="ssssss"
style="display:inline;fill:#313131;fill-opacity:1;stroke-width:4.41528;stroke-linecap:round"
d="m 83.448042,49.242516 c 3.928913,1.194349 5.945041,4.623512 5.525393,6.249833 -0.419635,1.626313 -1.586453,2.033818 -4.022559,1.188423 -2.300205,-0.798011 -8.680288,-3.429088 -17.467185,1.58346 -2.300578,1.312383 -4.485891,-1.340335 -2.373777,-3.072561 8.46599,-6.943249 13.605145,-7.38791 18.338128,-5.949155 z"
id="path456-3"
inkscape:label="right-eye" /></g><metadata
id="metadata30760"><rdf:RDF><cc:Work
rdf:about=""><dc:rights><cc:Agent><dc:title>Blobfox team (https://git.shadamethyst.xyz/adri326/blobfox), licensed under the Apache 2.0 License</dc:title></cc:Agent></dc:rights><dc:title>blobfox</dc:title><dc:creator><cc:Agent><dc:title>Feuerfuchs</dc:title></cc:Agent></dc:creator><dc:source>https://git.shadamethyst.xyz/adri326/blobfox</dc:source><dc:contributor><cc:Agent><dc:title>Shad Amethyst</dc:title></cc:Agent></dc:contributor></cc:Work></rdf:RDF></metadata></svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

Loading…
Cancel
Save