M .gitignore => .gitignore +1 -0
@@ 2,3 2,4 @@ original/
output/
Cargo.lock
target/
+blobfox-*.zip
M Cargo.toml => Cargo.toml +1 -0
@@ 20,3 20,4 @@ resvg = "0.23"
usvg = "0.23"
tiny-skia = "0.6"
png = "0.17"
+css-color-parser = "0.1.2"
M README.md => README.md +12 -0
@@ 41,9 41,21 @@ If you'd like to help, there are a few things that need attention outside of imp
- german shepherd
- collie
- sheep
+ - bird
- etc.
- 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
All the code, images and assets of this repository are made available under the Apache 2.0 license.
A snuggle.toml => snuggle.toml +4 -0
@@ 0,0 1,4 @@
+name = "snuggle"
+dx = -60
+dy = -40
+transform = "scale(1.02 1.02) translate(-1.5 -1)"
A species/blobfox/assets/snuggle_right.svg => species/blobfox/assets/snuggle_right.svg +49 -0
@@ 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>
M species/blobfox/species.toml => species/blobfox/species.toml +5 -0
@@ 6,6 6,7 @@ body_color = "#ff8702"
ear_color = "#313131"
ear_fluff_color = "#ebdccc"
hand_color = "#ff8702"
+hand_stroke_color = "#313131"
tail_color = "#ff8702"
[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_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"]
+
+# Snuggle
+snuggle_right = ["body-snuggle", "eyes-snuggle", "mouth-w"]
+snuggle_right_shadow = ["body-snuggle", "eyes-snuggle", "mouth-w"]
A species/blobfox/templates/body-snuggle.mustache => species/blobfox/templates/body-snuggle.mustache +15 -0
@@ 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}}
M species/blobfox/templates/body.mustache => species/blobfox/templates/body.mustache +3 -0
@@ 8,4 8,7 @@
{{#tags.body-comfy}}
{{>body-comfy}}
{{/tags.body-comfy}}
+ {{#tags.body-snuggle}}
+ {{>body-snuggle}}
+ {{/tags.body-snuggle}}
</g>
M species/blobfox/templates/eyes.mustache => species/blobfox/templates/eyes.mustache +4 -0
@@ 23,4 23,8 @@
{{#blush}}#left-eye{{/blush}}
{{#blush}}#right-eye{{/blush}}
{{/tags.eyes-closed}}
+ {{#tags.eyes-snuggle}}
+ {{#snuggle_right}}#left-eye{{/snuggle_right}}
+ {{#snuggle_right}}#right-eye{{/snuggle_right}}
+ {{/tags.eyes-snuggle}}
</g>
M species/blobfox/templates/hands.mustache => species/blobfox/templates/hands.mustache +14 -2
@@ 1,7 1,19 @@
<g id="hands">
{{#tags.hands-reach}}
- {{#set-fill}} {{vars.hand_color}} | {{#reach_aww}}#left-hand{{/reach_aww}} {{/set-fill}}
- {{#set-fill}} {{vars.hand_color}} | {{#reach_aww}}#right-hand{{/reach_aww}} {{/set-fill}}
+ {{#set-stroke}}
+ {{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.hand-3c}}
{{#tags.holding}}
M species/blobfox/templates/nose.mustache => species/blobfox/templates/nose.mustache +3 -0
@@ 14,6 14,9 @@
{{#tags.eyes-closed}}
{{#blush}}#nose-outline{{/blush}}
{{/tags.eyes-closed}}
+ {{#tags.eyes-snuggle}}
+ {{#snuggle_right}}#nose-outline{{/snuggle_right}}
+ {{/tags.eyes-snuggle}}
{{#base}}#nose{{/base}}
</g>
M species/blobfox/variants/heart_ace.mustache => species/blobfox/variants/heart_ace.mustache +5 -1
@@ 9,7 9,11 @@
</clipPath>
</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)">
{{#flag_ace}}{{/flag_ace}}
</g>
M species/blobfox/variants/heart_demisexual.mustache => species/blobfox/variants/heart_demisexual.mustache +5 -1
@@ 9,7 9,11 @@
</clipPath>
</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)">
{{#flag_demisexual}}{{/flag_demisexual}}
</g>
M species/blobfox/variants/heart_enby.mustache => species/blobfox/variants/heart_enby.mustache +5 -1
@@ 9,7 9,11 @@
</clipPath>
</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)">
{{#flag_enby}}{{/flag_enby}}
</g>
M species/blobfox/variants/heart_pan.mustache => species/blobfox/variants/heart_pan.mustache +5 -1
@@ 9,7 9,11 @@
</clipPath>
</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)">
{{#flag_pan}}{{/flag_pan}}
</g>
M species/blobfox/variants/heart_progress.mustache => species/blobfox/variants/heart_progress.mustache +5 -1
@@ 9,7 9,11 @@
</clipPath>
</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)">
{{#flag_progress}}{{/flag_progress}}
</g>
A species/blobfox/variants/snuggle_right.mustache => species/blobfox/variants/snuggle_right.mustache +9 -0
@@ 0,0 1,9 @@
+{{>header}}
+ {{>body}}
+
+ <g transform="rotate(-10 45 75)">
+ {{>eyes}}
+ {{>nose}}
+ {{>mouth}}
+ </g>
+{{>footer}}
A src/bin/snuggle.rs => src/bin/snuggle.rs +148 -0
@@ 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,
+ }
+ }
+}
M src/export.rs => src/export.rs +57 -13
@@ 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)> {
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> {
- let usvg_options = Options::default();
- Tree::from_str(svg_str, &usvg_options.to_ref())
-}
+/// Removes all elements marked with `blobfox-ignore-size="true"`
-fn get_xml(svg_str: &str) -> Result<Element, xmltree::ParseError> {
- Element::parse(svg_str.as_bytes())
-}
+macro_rules! strip {
+ ( $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> {
- let mut s: Vec<u8> = Vec::new();
+ fn rec(element: &mut Element) {
+ // 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> {
- 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)?;
svg_xml.attributes.insert(
"viewBox".to_string(),
@@ 144,12 186,14 @@ pub fn export(
mut svg_str: String,
output_dir: &PathBuf,
output_name: String,
- args: &super::Args,
+ args: &ExportArgs,
) -> Result<(), ExportError> {
if !args.no_resize {
svg_str = resize(svg_str)?;
}
+ svg_str = strip_only_size(&svg_str)?;
+
svg_str = combine_defs(svg_str)?;
mkdirp::mkdirp(output_dir.join("vector")).unwrap();
A src/lib.rs => src/lib.rs +3 -0
@@ 0,0 1,3 @@
+pub mod parse;
+pub mod template;
+pub mod export;
M src/main.rs => src/main.rs +18 -10
@@ 1,14 1,11 @@
use clap::Parser;
use std::path::PathBuf;
-pub mod parse;
-use parse::*;
-
-pub mod template;
-use template::*;
-
-pub mod export;
-use export::*;
+use blobfox_template::{
+ parse::*,
+ template::*,
+ export::*,
+};
fn main() {
let args = Args::parse();
@@ 30,6 27,8 @@ fn main() {
}
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) {
match context.compile(path).and_then(|template| {
template.render_data_to_string(&context.get_data(name))
@@ 39,7 38,7 @@ fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf
svg,
output_dir,
format!("{}_{}", context.species().name, name),
- args
+ &args
) {
Ok(_) => {}
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)]
pub struct Args {
/// A folder containing the declaration from which the emotes should be generated
@@ 79,3 78,12 @@ pub struct Args {
#[clap(short, long, value_parser)]
output_dir: Option<PathBuf>,
}
+
+impl From<Args> for ExportArgs {
+ fn from(args: Args) -> ExportArgs {
+ ExportArgs {
+ no_resize: args.no_resize,
+ dim: args.dim,
+ }
+ }
+}
M src/template.rs => src/template.rs +76 -49
@@ 1,9 1,10 @@
-use super::*;
+use crate::parse::SpeciesDecl;
use mustache::{Context, Data, MapBuilder, PartialLoader, Template};
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex};
use xmltree::{Element, XMLNode};
+use css_color_parser::Color as CssColor;
#[derive(Debug, Clone)]
pub struct RenderingContext {
@@ 90,39 91,45 @@ impl RenderingContext {
});
}
- let this = self.clone();
- let variant_name_owned = variant_name.to_string();
- builder = builder.insert_fn("set-fill", move |input| {
- // Parse `color|xml`
- if let [color, xml] = input.splitn(2, '|').collect::<Vec<_>>()[..] {
- // Render `color` and `xml`
- if let (Ok(color), Ok(xml)) = (
- this.render_to_string(&color, &variant_name_owned),
- this.render_to_string(&xml, &variant_name_owned),
- ) {
- // Convert `xml` to XML
- match Element::parse(xml.as_bytes()) {
- Ok(mut xml) => {
- set_fill(&color.trim(), &mut xml);
-
- // Render XML to string
- if let Some(res) = xml_to_string(xml) {
- res
- } else {
- String::from("<!-- Error in stringifying xml -->")
+ for (cb, name) in [
+ (set_fill as fn(&str, &mut Element), "set-fill"),
+ (set_stroke, "set-stroke")
+ ] {
+ let this = self.clone();
+ let variant_name_owned = variant_name.to_string();
+
+ builder = builder.insert_fn(name, move |input| {
+ // Parse `color|xml`
+ if let [color, xml] = input.splitn(2, '|').collect::<Vec<_>>()[..] {
+ // Render `color` and `xml`
+ if let (Ok(color), Ok(xml)) = (
+ this.render_to_string(&color, &variant_name_owned),
+ this.render_to_string(&xml, &variant_name_owned),
+ ) {
+ // Convert `xml` to XML
+ match Element::parse(xml.as_bytes()) {
+ Ok(mut xml) => {
+ cb(&color.trim(), &mut 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) => {
- format!("<!-- Error in parsing xml: {} -->", err)
- }
+ } else {
+ String::from("<!-- Error in parsing color or element -->")
}
} 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();
@@ 236,34 243,54 @@ impl PartialLoader for RenderingContext {
}
}
-fn set_fill(color: &str, xml: &mut Element) {
- // Substitute the fill color; TODO: handle transparency for SVG 1.1
- if let Some(style) = xml.attributes.get_mut("style") {
- let mut new_style = Vec::new();
+macro_rules! set_color {
+ ( $fn_name:tt, $color_name:expr, $opacity_name:expr ) => {
+ pub fn $fn_name(color: &str, xml: &mut Element) {
+ 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(';') {
- if let [name, value] = rule.splitn(2, ':').collect::<Vec<_>>()[..] {
- if name.trim() != "fill" && name.trim() != "fill-opacity" {
- new_style.push(format!("{}:{}", name, value));
- }
- }
- }
+ fn rec(color: &str, opacity: f32, xml: &mut Element) {
+ 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));
+ }
+ }
+ }
- 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(";");
- }
- if let Some(_fill) = xml.attributes.get("fill") {
- xml.attributes.insert("fill".to_string(), color.to_string());
- }
+ *style = new_style.join(";");
+ }
+ if let Some(_fill) = xml.attributes.get($color_name) {
+ 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() {
- if let XMLNode::Element(ref mut child) = child {
- set_fill(color, child);
+ rec(&color, opacity, xml)
}
}
}
+set_color!(set_fill, "fill", "fill-opacity");
+set_color!(set_stroke, "stroke", "stroke-opacity");
+
pub fn query_selector(svg: Element, pattern: &str) -> Option<Element> {
if pattern == "" {
// NOTE: it looks like having a nested svg makes resvg unhappy
A vector/blobfox_snuggle_right.svg => vector/blobfox_snuggle_right.svg +125 -0
@@ 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>