@ -1,2 +1,4 @@
|
|||||||
original/
|
original/
|
||||||
output/
|
output/
|
||||||
|
Cargo.lock
|
||||||
|
target/
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "blobfox-template"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
default-run = "blobfox-template"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.140", features = ["derive"] }
|
||||||
|
toml = "0.5.9"
|
||||||
|
xmltree = "0.10.3"
|
||||||
|
mustache = { git = "https://git.shadamethyst.xyz/adri326/rust-mustache.git" }
|
||||||
|
clap = { version = "3.2", features = ["derive"] }
|
||||||
|
mkdirp = "1.0.0"
|
||||||
|
resvg = "0.23"
|
||||||
|
usvg = "0.23"
|
||||||
|
tiny-skia = "0.6"
|
||||||
|
png = "0.17"
|
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cargo run --bin clean -- "$1"
|
||||||
|
xmllint "$1" --format --output "$1"
|
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 8.2 KiB |
@ -0,0 +1,10 @@
|
|||||||
|
name = "blobamber"
|
||||||
|
base = "../blobcat/"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
body_color = "#6a6862"
|
||||||
|
ear_color = "#313131"
|
||||||
|
ear_fluff_color = "#474747"
|
||||||
|
hand_color = "#6a6862"
|
||||||
|
tail_color = "#6a6862"
|
||||||
|
marks_color = "#c8c8c8"
|
@ -0,0 +1,3 @@
|
|||||||
|
{{>blobcat.body-basic}}
|
||||||
|
|
||||||
|
{{#set-fill}} {{vars.marks_color}} | {{#base}}#belly{{/base}} {{/set-fill}}
|
@ -0,0 +1,14 @@
|
|||||||
|
<!-- DEFS -->
|
||||||
|
|
||||||
|
{{#blobamber.owo}}#blobamber-owo-defs{{/blobamber.owo}}
|
||||||
|
{{#blobamber.base}}#blobamber-defs{{/blobamber.base}}
|
||||||
|
|
||||||
|
{{#tags.eyes-owo}}
|
||||||
|
<g id="eyes">
|
||||||
|
{{#owo}}#left-eye{{/owo}}
|
||||||
|
{{#owo}}#right-eye{{/owo}}
|
||||||
|
</g>
|
||||||
|
{{/tags.eyes-owo}}
|
||||||
|
{{^tags.eyes-owo}}
|
||||||
|
{{>blobcat.eyes}}
|
||||||
|
{{/tags.eyes-owo}}
|
@ -0,0 +1,14 @@
|
|||||||
|
{{#tags.hand-3c}}
|
||||||
|
{{#tags.holding}}
|
||||||
|
<g transform="translate(0.5 -1)">
|
||||||
|
{{/tags.holding}}
|
||||||
|
{{^tags.holding}}
|
||||||
|
<g transform="translate(1.2 -2.9)">
|
||||||
|
{{/tags.holding}}
|
||||||
|
{{/tags.hand-3c}}
|
||||||
|
|
||||||
|
{{>blobcat.hands}}
|
||||||
|
|
||||||
|
{{#tags.hand-3c}}
|
||||||
|
</g>
|
||||||
|
{{/tags.hand-3c}}
|
After Width: | Height: | Size: 7.6 KiB |
@ -0,0 +1,9 @@
|
|||||||
|
name = "blobcat"
|
||||||
|
base = "../blobfox/"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
body_color = "#fcc21b"
|
||||||
|
ear_color = "#e9ae20"
|
||||||
|
ear_fluff_color = "#8a6135"
|
||||||
|
hand_color = "#fcc21b"
|
||||||
|
tail_color = "#fcc21b"
|
@ -0,0 +1,14 @@
|
|||||||
|
{{! Pull the defs from the svg for the gradients }}
|
||||||
|
{{#blush}}#blush-defs{{/blush}}
|
||||||
|
|
||||||
|
<g transform="translate(0.5 0.9)">
|
||||||
|
{{#blush}}#left-blush{{/blush}}
|
||||||
|
{{#blush}}#left-blush-line{{/blush}}
|
||||||
|
{{#blush}}#left-blush-line-2{{/blush}}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(0.0 0.1)">
|
||||||
|
{{#blush}}#right-blush{{/blush}}
|
||||||
|
{{#blush}}#right-blush-line{{/blush}}
|
||||||
|
{{#blush}}#right-blush-line-2{{/blush}}
|
||||||
|
</g>
|
@ -0,0 +1,20 @@
|
|||||||
|
{{! Left ear }}
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#base}}#left-ear{{/base}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{! Body }}
|
||||||
|
<defs>
|
||||||
|
<clipPath id="body-clip">
|
||||||
|
{{#base}}#body{{/base}}
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#base}}#body{{/base}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#base}}#hair{{/base}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{! Right ear }}
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#base}}#right-ear{{/base}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.ear_fluff_color}} | {{#base}}#right-ear-fluff{{/base}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{! Whiskers }}
|
||||||
|
{{^tags.hands-reach}}
|
||||||
|
{{>whiskers}}
|
||||||
|
{{/tags.hands-reach}}
|
@ -0,0 +1,23 @@
|
|||||||
|
<g id="body-snug">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="body-clip">
|
||||||
|
{{#snug}}#body{{/snug}}
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<g id="left-ear" transform="translate(0 2) rotate(5 6.6 5.5) translate(0 0.2)">
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#base}}#left-ear{{/base}} {{/set-fill}}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{{#set-fill}}
|
||||||
|
{{vars.body_color}}
|
||||||
|
| {{#snug}}#body{{/snug}}
|
||||||
|
{{/set-fill}}
|
||||||
|
|
||||||
|
<g id="right-ear" transform="scale(1 0.95) translate(0 2) rotate(5 29 5) translate(-0.5 1.6)">
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#base}}#right-ear{{/base}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.ear_fluff_color}} | {{#base}}#right-ear-fluff{{/base}} {{/set-fill}}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{{>tail}}
|
||||||
|
</g>
|
@ -0,0 +1,3 @@
|
|||||||
|
<g id="hand-boop" transform="translate(1.5 -1)">
|
||||||
|
{{>blobfox.hand-boop}}
|
||||||
|
</g>
|
@ -0,0 +1,3 @@
|
|||||||
|
{{#set-fill}}
|
||||||
|
{{vars.hand_color}} | {{>blobfox.hands}}
|
||||||
|
{{/set-fill}}
|
@ -0,0 +1,13 @@
|
|||||||
|
{{#tags.boop}}
|
||||||
|
<g transform="translate(-0.2 0.5)">
|
||||||
|
{{/tags.boop}}
|
||||||
|
{{#tags.mouth-hmpf}}
|
||||||
|
<g transform="translate(0.5 0)">
|
||||||
|
{{/tags.mouth-hmpf}}
|
||||||
|
{{>blobfox.mouth}}
|
||||||
|
{{#tags.boop}}
|
||||||
|
</g>
|
||||||
|
{{/tags.boop}}
|
||||||
|
{{#tags.mouth-hmpf}}
|
||||||
|
</g>
|
||||||
|
{{/tags.mouth-hmpf}}
|
@ -0,0 +1,17 @@
|
|||||||
|
{{#tags.body-snug}}
|
||||||
|
<g transform="translate(1.5 0)">
|
||||||
|
{{#blobfox.base}}#nose{{/blobfox.base}}
|
||||||
|
</g>
|
||||||
|
{{/tags.body-snug}}
|
||||||
|
|
||||||
|
{{#tags.boop}}
|
||||||
|
<g transform="translate(1.5 -0.5)">
|
||||||
|
{{#blobfox.base}}#nose{{/blobfox.base}}
|
||||||
|
</g>
|
||||||
|
{{/tags.boop}}
|
||||||
|
|
||||||
|
{{#tags.mouth-hmpf}}
|
||||||
|
<g transform="translate(0.7 -0.2)">
|
||||||
|
{{#blobfox.base}}#nose{{/blobfox.base}}
|
||||||
|
</g>
|
||||||
|
{{/tags.mouth-hmpf}}
|
@ -0,0 +1,4 @@
|
|||||||
|
{{#set-fill}}
|
||||||
|
{{vars.tail_color}}
|
||||||
|
| {{>blobfox.tail}}
|
||||||
|
{{/set-fill}}
|
@ -0,0 +1,14 @@
|
|||||||
|
<g id="whiskers">
|
||||||
|
{{#tags.left-hand}}{{#tags.right-hand}}{{#tags.holding}}
|
||||||
|
<g transform="translate(0 -1)"><!-- HELLO? -->
|
||||||
|
{{/tags.holding}}{{/tags.right-hand}}{{/tags.left-hand}}
|
||||||
|
|
||||||
|
{{#base}}#left-whisker{{/base}}
|
||||||
|
{{#base}}#left-whisker-2{{/base}}
|
||||||
|
{{#base}}#right-whisker{{/base}}
|
||||||
|
{{#base}}#right-whisker-2{{/base}}
|
||||||
|
|
||||||
|
{{#tags.left-hand}}{{#tags.right-hand}}{{#tags.holding}}
|
||||||
|
</g>
|
||||||
|
{{/tags.holding}}{{/tags.right-hand}}{{/tags.left-hand}}
|
||||||
|
</g>
|
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 8.5 KiB |
@ -0,0 +1,38 @@
|
|||||||
|
# Add options in here as needs be
|
||||||
|
name = "blobfox"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
body_color = "#ff8702"
|
||||||
|
ear_color = "#313131"
|
||||||
|
ear_fluff_color = "#ebdccc"
|
||||||
|
hand_color = "#ff8702"
|
||||||
|
tail_color = "#ff8702"
|
||||||
|
|
||||||
|
[variants]
|
||||||
|
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"]
|
||||||
|
|
||||||
|
"3c" = ["body-basic", "eyes-basic", "mouth-w", "hand-3c", "left-hand"]
|
||||||
|
"3c_evil" = ["body-basic", "eyes-evil", "mouth-w", "hand-3c", "left-hand"]
|
||||||
|
|
||||||
|
boop = ["body-basic", "boop", "eyes-basic", "mouth-w"]
|
||||||
|
boop_aww = ["body-basic", "boop", "eyes-aww", "mouth-w"]
|
||||||
|
boop_owo = ["body-basic", "ear-owo", "boop", "eyes-owo", "mouth-w"]
|
||||||
|
|
||||||
|
reach = ["body-basic", "eyes-basic", "mouth-w", "hands-reach", "left-hand", "right-hand"]
|
||||||
|
reach_aww = ["body-basic", "eyes-aww", "mouth-w", "hands-reach", "left-hand", "right-hand"]
|
||||||
|
reach_owo = ["body-basic", "ear-owo", "eyes-owo", "mouth-w", "hands-reach", "left-hand", "right-hand"]
|
||||||
|
|
||||||
|
snug = ["body-snug", "eyes-happy", "tail"]
|
||||||
|
snug_aww = ["body-snug", "eyes-aww", "tail"]
|
||||||
|
snug_owo = ["body-snug", "ear-owo", "eyes-owo", "tail"]
|
||||||
|
snug_boop_owo = ["body-snug", "ear-owo", "eyes-owo", "tail", "boop"]
|
||||||
|
|
||||||
|
stabby = ["body-basic", "holding", "eyes-evil", "mouth-w", "hand-3c", "left-hand"]
|
||||||
|
|
||||||
|
blush = ["body-basic", "ear-blush", "eyes-closed", "blush", "mouth-hmpf"]
|
||||||
|
|
||||||
|
heart = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding", "big-object"]
|
||||||
|
egg = ["body-basic", "eyes-basic", "left-hand", "right-hand", "holding"]
|
@ -0,0 +1,10 @@
|
|||||||
|
{{! Pull the defs from the svg for the gradients }}
|
||||||
|
{{#blush}}#blush-defs{{/blush}}
|
||||||
|
|
||||||
|
{{#blush}}#right-blush{{/blush}}
|
||||||
|
{{#blush}}#right-blush-line{{/blush}}
|
||||||
|
{{#blush}}#right-blush-line-2{{/blush}}
|
||||||
|
|
||||||
|
{{#blush}}#left-blush{{/blush}}
|
||||||
|
{{#blush}}#left-blush-line{{/blush}}
|
||||||
|
{{#blush}}#left-blush-line-2{{/blush}}
|
@ -0,0 +1,21 @@
|
|||||||
|
{{! Left ear }}
|
||||||
|
{{#tags.ear-owo}}
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#boop_owo}}#left-ear{{/boop_owo}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.ear_fluff_color}} | {{#boop_owo}}#left-ear-fluff{{/boop_owo}} {{/set-fill}}
|
||||||
|
{{/tags.ear-owo}}
|
||||||
|
{{^tags.ear-owo}}
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#base}}#left-ear{{/base}} {{/set-fill}}
|
||||||
|
{{/tags.ear-owo}}
|
||||||
|
|
||||||
|
{{! Body }}
|
||||||
|
<defs>
|
||||||
|
<clipPath id="body-clip">
|
||||||
|
{{#base}}#body{{/base}}
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#base}}#body{{/base}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#base}}#hair{{/base}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{! Right ear }}
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#base}}#right-ear{{/base}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.ear_fluff_color}} | {{#base}}#right-ear-fluff{{/base}} {{/set-fill}}
|
@ -0,0 +1,36 @@
|
|||||||
|
<g id="body-snug">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="body-clip">
|
||||||
|
{{#snug}}#body{{/snug}}
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{{! TODO: owo ear }}
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#snug}}#left-ear{{/snug}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#snug}}#body{{/snug}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#snug}}#hair{{/snug}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#snug}}#right-ear{{/snug}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.ear_fluff_color}} | {{#snug}}#right-ear-fluff{{/snug}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{#tags.tail}}
|
||||||
|
<g id="tail">
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#snug}}#tail{{/snug}} {{/set-fill}}
|
||||||
|
</g>
|
||||||
|
{{/tags.tail}}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Following is a list of transforms to approximate the snug version using the base version
|
||||||
|
<g id="new" transform="scale(1 0.88) translate(0 2)">
|
||||||
|
<g id="left-ear" transform="rotate(5 6.6 5.5) translate(0 0.2) scale(1 1.2)">
|
||||||
|
{{#base}}#left-ear{{/base}}
|
||||||
|
</g>
|
||||||
|
{{#base}}#body{{/base}}
|
||||||
|
{{#base}}#hair{{/base}}
|
||||||
|
<g id="right-ear" transform="rotate(5 29 5) translate(-0.5 1.6)">
|
||||||
|
{{#base}}#right-ear{{/base}}
|
||||||
|
{{#base}}#right-ear-fluff{{/base}}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
-->
|
@ -0,0 +1,8 @@
|
|||||||
|
<g id="body">
|
||||||
|
{{#tags.body-basic}}
|
||||||
|
{{>body-basic}}
|
||||||
|
{{/tags.body-basic}}
|
||||||
|
{{#tags.body-snug}}
|
||||||
|
{{>body-snug}}
|
||||||
|
{{/tags.body-snug}}
|
||||||
|
</g>
|
@ -0,0 +1,26 @@
|
|||||||
|
<g id="eyes">
|
||||||
|
{{#tags.eyes-basic}}
|
||||||
|
{{#base}}#left-eye{{/base}}
|
||||||
|
{{#base}}#right-eye{{/base}}
|
||||||
|
{{/tags.eyes-basic}}
|
||||||
|
{{#tags.eyes-owo}}
|
||||||
|
{{#boop_owo}}#left-eye{{/boop_owo}}
|
||||||
|
{{#boop_owo}}#right-eye{{/boop_owo}}
|
||||||
|
{{/tags.eyes-owo}}
|
||||||
|
{{#tags.eyes-happy}}
|
||||||
|
{{#snug}}#left-eye{{/snug}}
|
||||||
|
{{#snug}}#right-eye{{/snug}}
|
||||||
|
{{/tags.eyes-happy}}
|
||||||
|
{{#tags.eyes-aww}}
|
||||||
|
{{#reach_aww}}#left-eye{{/reach_aww}}
|
||||||
|
{{#reach_aww}}#right-eye{{/reach_aww}}
|
||||||
|
{{/tags.eyes-aww}}
|
||||||
|
{{#tags.eyes-evil}}
|
||||||
|
{{#3c_evil}}#left-eye{{/3c_evil}}
|
||||||
|
{{#3c_evil}}#right-eye{{/3c_evil}}
|
||||||
|
{{/tags.eyes-evil}}
|
||||||
|
{{#tags.eyes-closed}}
|
||||||
|
{{#blush}}#left-eye{{/blush}}
|
||||||
|
{{#blush}}#right-eye{{/blush}}
|
||||||
|
{{/tags.eyes-closed}}
|
||||||
|
</g>
|
@ -0,0 +1,2 @@
|
|||||||
|
</g>
|
||||||
|
</svg>
|
@ -0,0 +1 @@
|
|||||||
|
{{#boop_owo}}#hand-boop{{/boop_owo}}
|
@ -0,0 +1,26 @@
|
|||||||
|
<g id="hands">
|
||||||
|
{{#tags.hands-reach}}
|
||||||
|
{{#reach_aww}}#left-hand{{/reach_aww}}
|
||||||
|
{{#reach_aww}}#right-hand{{/reach_aww}}
|
||||||
|
{{/tags.hands-reach}}
|
||||||
|
{{#tags.hand-3c}}
|
||||||
|
{{#tags.holding}}
|
||||||
|
{{#knife}}#left-hand{{/knife}}
|
||||||
|
{{/tags.holding}}
|
||||||
|
{{^tags.holding}}
|
||||||
|
{{! :3c hand }}
|
||||||
|
{{#3c_evil}}#left-hand{{/3c_evil}}
|
||||||
|
{{/tags.holding}}
|
||||||
|
{{/tags.hand-3c}}
|
||||||
|
{{^tags.hand-3c}}
|
||||||
|
{{#tags.holding}}
|
||||||
|
{{#tags.left-hand}}
|
||||||
|
{{#heart}}#left-hand{{/heart}}
|
||||||
|
{{/tags.left-hand}}
|
||||||
|
|
||||||
|
{{#tags.right-hand}}
|
||||||
|
{{#heart}}#right-hand{{/heart}}
|
||||||
|
{{/tags.right-hand}}
|
||||||
|
{{/tags.holding}}
|
||||||
|
{{/tags.hand-3c}}
|
||||||
|
</g>
|
After Width: | Height: | Size: 389 B |
@ -0,0 +1,6 @@
|
|||||||
|
<g id="knife">
|
||||||
|
{{#knife}}#blade-back{{/knife}}
|
||||||
|
{{#knife}}#blade-front{{/knife}}
|
||||||
|
{{#knife}}#handle{{/knife}}
|
||||||
|
{{#knife}}#handle-screw{{/knife}}
|
||||||
|
</g>
|
@ -0,0 +1,6 @@
|
|||||||
|
{{#tags.mouth-w}}
|
||||||
|
{{#base}}#mouth{{/base}}
|
||||||
|
{{/tags.mouth-w}}
|
||||||
|
{{#tags.mouth-hmpf}}
|
||||||
|
{{#blush}}#mouth{{/blush}}
|
||||||
|
{{/tags.mouth-hmpf}}
|
@ -0,0 +1,19 @@
|
|||||||
|
<g id="nose">
|
||||||
|
{{#tags.eyes-basic}}
|
||||||
|
{{#base}}#nose-outline{{/base}}
|
||||||
|
{{/tags.eyes-basic}}
|
||||||
|
{{#tags.eyes-owo}}
|
||||||
|
{{#boop_owo}}#nose-outline{{/boop_owo}}
|
||||||
|
{{/tags.eyes-owo}}
|
||||||
|
{{#tags.eyes-aww}}
|
||||||
|
{{#reach_aww}}#nose-outline{{/reach_aww}}
|
||||||
|
{{/tags.eyes-aww}}
|
||||||
|
{{#tags.eyes-evil}}
|
||||||
|
{{#3c_evil}}#nose-outline{{/3c_evil}}
|
||||||
|
{{/tags.eyes-evil}}
|
||||||
|
{{#tags.eyes-closed}}
|
||||||
|
{{#blush}}#nose-outline{{/blush}}
|
||||||
|
{{/tags.eyes-closed}}
|
||||||
|
|
||||||
|
{{#base}}#nose{{/base}}
|
||||||
|
</g>
|
@ -0,0 +1 @@
|
|||||||
|
{{#snug}}#tail{{/snug}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>hands}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>hands}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,7 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>blush}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>hand-boop}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>hand-boop}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>hand-boop}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,10 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
|
||||||
|
{{#egg}}#egg{{/egg}}
|
||||||
|
{{#egg}}#egg-reflection{{/egg}}
|
||||||
|
|
||||||
|
{{>hands}}
|
||||||
|
{{>footer}}
|
@ -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}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
|
||||||
|
{{#heart}}#heart{{/heart}}
|
||||||
|
{{#heart}}#heart-reflection{{/heart}}
|
||||||
|
|
||||||
|
{{>hands}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,7 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>hands}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>hands}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>hands}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,6 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,6 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,8 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
|
||||||
|
{{>hand-boop}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,6 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>footer}}
|
@ -0,0 +1,10 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>body}}
|
||||||
|
|
||||||
|
{{>eyes}}
|
||||||
|
{{>nose}}
|
||||||
|
{{>mouth}}
|
||||||
|
|
||||||
|
{{>knife}}
|
||||||
|
{{>hands}}
|
||||||
|
{{>footer}}
|
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,10 @@
|
|||||||
|
name = "blobstella"
|
||||||
|
base = "../blobcat/"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
body_color = "#54b6e7"
|
||||||
|
ear_color = "#3398c7"
|
||||||
|
ear_fluff_color = "#224f66"
|
||||||
|
hand_color = "#54b6e7"
|
||||||
|
tail_color = "#54b6e7"
|
||||||
|
marks_color = "#eee64e"
|
@ -0,0 +1,26 @@
|
|||||||
|
{{! Left ear }}
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#base}}#left-ear{{/base}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{! Body }}
|
||||||
|
<defs>
|
||||||
|
<clipPath id="body-clip">
|
||||||
|
{{#base}}#body{{/base}}
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#base}}#body{{/base}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.body_color}} | {{#base}}#hair{{/base}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{! Right ear }}
|
||||||
|
{{#set-fill}} {{vars.ear_color}} | {{#base}}#right-ear{{/base}} {{/set-fill}}
|
||||||
|
{{#set-fill}} {{vars.ear_fluff_color}} | {{#base}}#right-ear-full{{/base}} {{/set-fill}}
|
||||||
|
|
||||||
|
{{! Marks }}
|
||||||
|
<g clip-path="url(#body-clip)">
|
||||||
|
{{#base}}#left-marks{{/base}}
|
||||||
|
{{#base}}#right-marks{{/base}}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{{! Whiskers }}
|
||||||
|
{{^tags.hands-reach}}
|
||||||
|
{{>whiskers}}
|
||||||
|
{{/tags.hands-reach}}
|
@ -0,0 +1,14 @@
|
|||||||
|
{{>blobcat.body-snug}}
|
||||||
|
|
||||||
|
<g clip-path="url(#body-clip)">
|
||||||
|
<g transform="scale(1 0.88) translate(0 4)">
|
||||||
|
{{#set-fill}}
|
||||||
|
{{vars.marks_color}}
|
||||||
|
| {{#base}}#left-marks{{/base}}
|
||||||
|
{{/set-fill}}
|
||||||
|
{{#set-fill}}
|
||||||
|
{{vars.marks_color}}
|
||||||
|
| {{#base}}#right-marks{{/base}}
|
||||||
|
{{/set-fill}}
|
||||||
|
</g>
|
||||||
|
</g>
|
@ -0,0 +1,69 @@
|
|||||||
|
use xmltree::{XMLNode, Element};
|
||||||
|
use clap::Parser;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
for path in args.files {
|
||||||
|
let file = std::fs::File::open(path.clone()).unwrap_or_else(|err| {
|
||||||
|
panic!("Error while reading {}: {}", path.display(), err);
|
||||||
|
});
|
||||||
|
let mut element = Element::parse(file).expect("Couldn't parse SVG!");
|
||||||
|
|
||||||
|
clean(&mut element);
|
||||||
|
|
||||||
|
let mut s: Vec<u8> = Vec::new();
|
||||||
|
element.write(&mut s).expect("Couldn't export SVG!");
|
||||||
|
|
||||||
|
std::fs::write(path.clone(), s).unwrap_or_else(|err| {
|
||||||
|
panic!("Error while writing {}: {}", path.display(), err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean(element: &mut Element) {
|
||||||
|
let mut counts: HashMap<String, usize> = HashMap::new();
|
||||||
|
|
||||||
|
fn count_rec(element: &Element, counts: &mut HashMap<String, usize>) {
|
||||||
|
if let Some(label) = element.attributes.get("label") {
|
||||||
|
if let Some(count) = counts.get_mut(label) {
|
||||||
|
*count += 1;
|
||||||
|
} else {
|
||||||
|
counts.insert(label.to_string(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in element.children.iter() {
|
||||||
|
if let XMLNode::Element(ref child) = child {
|
||||||
|
count_rec(child, counts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count_rec(element, &mut counts);
|
||||||
|
|
||||||
|
fn update_rec(element: &mut Element, counts: &HashMap<String, usize>) {
|
||||||
|
if let Some(label) = element.attributes.get("label") {
|
||||||
|
if let Some(1) = counts.get(label) {
|
||||||
|
element.attributes.insert("id".to_string(), label.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in element.children.iter_mut() {
|
||||||
|
if let XMLNode::Element(ref mut child) = child {
|
||||||
|
update_rec(child, counts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_rec(element, &counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
#[clap(value_parser)]
|
||||||
|
files: Vec<PathBuf>,
|
||||||
|
}
|
@ -0,0 +1,178 @@
|
|||||||
|
use usvg::{
|
||||||
|
Tree,
|
||||||
|
NodeExt,
|
||||||
|
Options,
|
||||||
|
};
|
||||||
|
use xmltree::{XMLNode, Element};
|
||||||
|
use std::path::{PathBuf};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ExportError {
|
||||||
|
Xml(xmltree::Error),
|
||||||
|
XmlParse(xmltree::ParseError),
|
||||||
|
Usvg(usvg::Error),
|
||||||
|
Io(PathBuf, std::io::Error),
|
||||||
|
NoBBox,
|
||||||
|
Utf8(std::string::FromUtf8Error),
|
||||||
|
Encode(png::EncodingError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xmltree::ParseError> for ExportError {
|
||||||
|
fn from(err: xmltree::ParseError) -> Self {
|
||||||
|
Self::XmlParse(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xmltree::Error> for ExportError {
|
||||||
|
fn from(err: xmltree::Error) -> Self {
|
||||||
|
Self::Xml(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<usvg::Error> for ExportError {
|
||||||
|
fn from(err: usvg::Error) -> Self {
|
||||||
|
Self::Usvg(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::string::FromUtf8Error> for ExportError {
|
||||||
|
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||||
|
Self::Utf8(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<png::EncodingError> for ExportError {
|
||||||
|
fn from(err: png::EncodingError) -> Self {
|
||||||
|
Self::Encode(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_new_bbox(svg: &Tree) -> Option<(f64, f64, f64, f64)> {
|
||||||
|
let bbox = svg.root().calculate_bbox()?;
|
||||||
|
|
||||||
|
// FIXME: remove once https://github.com/RazrFalcon/resvg/issues/528 is fixed
|
||||||
|
const MARGIN: f64 = 1.0;
|
||||||
|
|
||||||
|
let x = bbox.x() - MARGIN;
|
||||||
|
let y = bbox.y() - MARGIN;
|
||||||
|
let width = bbox.width() + MARGIN * 2.0;
|
||||||
|
let height = bbox.height() + MARGIN * 2.0;
|
||||||
|
|
||||||
|
if width > height {
|
||||||
|
let y = y - (width - height) / 2.0;
|
||||||
|
|
||||||
|
Some((x, y, width, width))
|
||||||
|
} else {
|
||||||
|
let x = x - (height - width) / 2.0;
|
||||||
|
|
||||||
|
Some((x, y, height, height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_xml(svg_str: &str) -> Result<Element, xmltree::ParseError> {
|
||||||
|
Element::parse(svg_str.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(svg_str: String) -> Result<String, ExportError> {
|
||||||
|
if let Some(new_bbox) = get_new_bbox(&get_usvg(&svg_str)?) {
|
||||||
|
let mut svg_xml = get_xml(&svg_str)?;
|
||||||
|
svg_xml.attributes.insert(
|
||||||
|
"viewBox".to_string(),
|
||||||
|
format!("{} {} {} {}", new_bbox.0, new_bbox.1, new_bbox.2, new_bbox.3),
|
||||||
|
);
|
||||||
|
|
||||||
|
xml_to_str(&svg_xml)
|
||||||
|
} else {
|
||||||
|
Err(ExportError::NoBBox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds all the `<defs>` in the svg and combines them all into one
|
||||||
|
pub fn combine_defs(svg_str: String) -> Result<String, ExportError> {
|
||||||
|
let mut svg_xml = get_xml(&svg_str)?;
|
||||||
|
|
||||||
|
let mut defs = Vec::new();
|
||||||
|
|
||||||
|
fn collect_defs(element: &mut Element, defs: &mut Vec<Element>) {
|
||||||
|
for child in std::mem::take(&mut element.children) {
|
||||||
|
match child {
|
||||||
|
XMLNode::Element(child) if child.name == "defs" => {
|
||||||
|
defs.push(child);
|
||||||
|
}
|
||||||
|
mut child => {
|
||||||
|
if let XMLNode::Element(ref mut child) = &mut child {
|
||||||
|
collect_defs(child, defs);
|
||||||
|
}
|
||||||
|
element.children.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_defs(&mut svg_xml, &mut defs);
|
||||||
|
|
||||||
|
let mut defs_element = Element::new("defs");
|
||||||
|
defs_element.children = defs
|
||||||
|
.into_iter()
|
||||||
|
.map(|def| {
|
||||||
|
def.children.into_iter().filter(|child| matches!(child, XMLNode::Element(_)))
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
defs_element.attributes.insert("id".to_string(), "defs".to_string());
|
||||||
|
|
||||||
|
svg_xml.children.insert(0, XMLNode::Element(defs_element));
|
||||||
|
|
||||||
|
xml_to_str(&svg_xml)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export(
|
||||||
|
mut svg_str: String,
|
||||||
|
output_dir: &PathBuf,
|
||||||
|
output_name: String,
|
||||||
|
args: &super::Args,
|
||||||
|
) -> Result<(), ExportError> {
|
||||||
|
if !args.no_resize {
|
||||||
|
svg_str = resize(svg_str)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg_str = combine_defs(svg_str)?;
|
||||||
|
|
||||||
|
mkdirp::mkdirp(output_dir.join("vector")).unwrap();
|
||||||
|
|
||||||
|
let output = output_dir.join(&format!("vector/{}.svg", output_name));
|
||||||
|
std::fs::write(output.clone(), svg_str.clone()).map_err(|err| ExportError::Io(output, err))?;
|
||||||
|
|
||||||
|
let svg_usvg = get_usvg(&svg_str)?;
|
||||||
|
for resolution in args.dim.iter().copied().filter(|r| *r != 0).collect::<HashSet<_>>() {
|
||||||
|
mkdirp::mkdirp(output_dir.join(&format!("{}", resolution))).unwrap();
|
||||||
|
let output = output_dir.join(&format!("{}/{}.png", resolution, output_name));
|
||||||
|
|
||||||
|
let mut image = tiny_skia::Pixmap::new(resolution, resolution).unwrap();
|
||||||
|
|
||||||
|
resvg::render(
|
||||||
|
&svg_usvg,
|
||||||
|
usvg::FitTo::Width(resolution),
|
||||||
|
tiny_skia::Transform::identity(),
|
||||||
|
image.as_mut()
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
image.save_png(output)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub mod parse;
|
||||||
|
use parse::*;
|
||||||
|
|
||||||
|
pub mod template;
|
||||||
|
use template::*;
|
||||||
|
|
||||||
|
pub mod export;
|
||||||
|
use export::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let species = load_species(args.decl.clone()).unwrap();
|
||||||
|
let context = RenderingContext::new(species);
|
||||||
|
|
||||||
|
let output_dir = args.output_dir.clone().unwrap_or(PathBuf::from("output/"));
|
||||||
|
|
||||||
|
if args.names.is_empty() {
|
||||||
|
for name in context.species().variant_paths.keys() {
|
||||||
|
generate_variant(&context, name, &output_dir, &args);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for name in args.names.iter() {
|
||||||
|
generate_variant(&context, name, &output_dir, &args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf, args: &Args) {
|
||||||
|
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))
|
||||||
|
}) {
|
||||||
|
Ok(svg) => {
|
||||||
|
match export(
|
||||||
|
svg,
|
||||||
|
output_dir,
|
||||||
|
format!("{}_{}", context.species().name, name),
|
||||||
|
args
|
||||||
|
) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error while rendering {}: {:?}", name, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error while rendering {}: {}", name, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("No variant named {}!", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
pub struct Args {
|
||||||
|
/// A folder containing the declaration from which the emotes should be generated
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
decl: PathBuf,
|
||||||
|
|
||||||
|
/// List of the emote names to export
|
||||||
|
#[clap(value_parser)]
|
||||||
|
names: Vec<String>,
|
||||||
|
|
||||||
|
/// Disable automatically resizing the SVG's viewBox, defaults to false
|
||||||
|
#[clap(short, long, value_parser, default_value = "false")]
|
||||||
|
no_resize: bool,
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
use xmltree::{Element};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use std::path::{PathBuf, Path};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Error returned upon failing to parse something
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ParseError {
|
||||||
|
Io(PathBuf, std::io::Error),
|
||||||
|
XmlParse(xmltree::ParseError),
|
||||||
|
Toml(toml::de::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<xmltree::ParseError> for ParseError {
|
||||||
|
fn from(err: xmltree::ParseError) -> Self {
|
||||||
|
Self::XmlParse(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for ParseError {
|
||||||
|
fn from(err: toml::de::Error) -> Self {
|
||||||
|
Self::Toml(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SpeciesDecl {
|
||||||
|
/// Imports xml and svg files from this folder if they aren't found
|
||||||
|
pub base: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// The name of the species
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub variants: HashMap<String, Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub vars: HashMap<String, String>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub template_paths: HashMap<String, PathBuf>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub variant_paths: HashMap<String, PathBuf>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub asset_paths: HashMap<String, PathBuf>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub parent: Option<Box<SpeciesDecl>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the given file as an XML tree
|
||||||
|
pub fn load_xml(path: impl AsRef<Path>) -> Result<Element, ParseError> {
|
||||||
|
let file = std::fs::File::open(path.as_ref()).map_err(|err| {
|
||||||
|
ParseError::Io(path.as_ref().to_path_buf(), err)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Element::parse(file)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the basic description of a SpeciesDecl
|
||||||
|
pub fn load_species(path: impl AsRef<Path>) -> Result<SpeciesDecl, ParseError> {
|
||||||
|
let declaration_path = path.as_ref().join("species.toml");
|
||||||
|
let declaration = std::fs::read_to_string(&declaration_path).map_err(|err| {
|
||||||
|
ParseError::Io(declaration_path, err)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut res: SpeciesDecl = toml::from_str(&declaration)?;
|
||||||
|
|
||||||
|
if let Some(ref base) = &res.base {
|
||||||
|
let path = path.as_ref().to_path_buf().join(base);
|
||||||
|
let base = load_species(path)?;
|
||||||
|
|
||||||
|
res.template_paths = base.template_paths.clone();
|
||||||
|
res.variant_paths = base.variant_paths.clone();
|
||||||
|
res.asset_paths = base.asset_paths.clone();
|
||||||
|
res.variants = base.variants.clone();
|
||||||
|
for (key, value) in base.vars.iter() {
|
||||||
|
if !res.vars.contains_key(key) {
|
||||||
|
res.vars.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.parent = Some(Box::new(base));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the `templates` directory and populate the `template_paths` field;
|
||||||
|
// on error, ignore the directory.
|
||||||
|
for (name, path) in read_dir_xml(path.as_ref().join("templates")) {
|
||||||
|
res.template_paths.insert(name, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the `variants` directory
|
||||||
|
for (name, path) in read_dir_xml(path.as_ref().join("variants")) {
|
||||||
|
res.variant_paths.insert(name, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the `assets` directory
|
||||||
|
for (name, path) in read_dir_xml(path.as_ref().join("assets")) {
|
||||||
|
res.asset_paths.insert(name, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_dir_xml(path: impl AsRef<Path>) -> HashMap<String, PathBuf> {
|
||||||
|
let mut res = HashMap::new();
|
||||||
|
|
||||||
|
if let Ok(iter) = std::fs::read_dir(path) {
|
||||||
|
for entry in iter.filter_map(|x| x.ok()) {
|
||||||
|
match (entry.path().file_stem(), entry.path().extension()) {
|
||||||
|
(Some(name), Some(ext)) => {
|
||||||
|
if matches!(ext.to_str(), Some("xml") | Some("svg") | Some("mustache")) {
|
||||||
|
if let Some(name) = name.to_str() {
|
||||||
|
res.insert(
|
||||||
|
name.to_string(),
|
||||||
|
entry.path().to_path_buf()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
@ -0,0 +1,294 @@
|
|||||||
|
use super::*;
|
||||||
|
use mustache::{Context, Data, MapBuilder, PartialLoader, Template};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use xmltree::{Element, XMLNode};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RenderingContext {
|
||||||
|
species: Arc<SpeciesDecl>,
|
||||||
|
|
||||||
|
rendered_variants: Arc<Mutex<HashMap<String, Element>>>,
|
||||||
|
|
||||||
|
loaded_assets: Arc<Mutex<HashMap<String, Element>>>,
|
||||||
|
|
||||||
|
parent: Option<Box<RenderingContext>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderingContext {
|
||||||
|
pub fn new(mut species: SpeciesDecl) -> Self {
|
||||||
|
let parent = std::mem::take(&mut species.parent).map(|parent| {
|
||||||
|
Box::new(Self::new(*parent))
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
species: Arc::new(species),
|
||||||
|
rendered_variants: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
loaded_assets: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compile(&self, path: impl AsRef<Path>) -> Result<Template<Self>, mustache::Error> {
|
||||||
|
let template = std::fs::read_to_string(path)?;
|
||||||
|
Context::with_loader(self.clone()).compile(template.chars())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_to_string(
|
||||||
|
&self,
|
||||||
|
string: &str,
|
||||||
|
variant_name: &str,
|
||||||
|
) -> Result<String, mustache::Error> {
|
||||||
|
Context::with_loader(self.clone())
|
||||||
|
.compile(string.chars())?
|
||||||
|
.render_data_to_string(&self.get_data(variant_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_data(&self, variant_name: &str) -> Data {
|
||||||
|
self.get_builder(variant_name, true).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_builder(&self, variant_name: &str, include_parent: bool) -> MapBuilder {
|
||||||
|
let mut builder = MapBuilder::new();
|
||||||
|
|
||||||
|
builder = builder.insert_map("variant", |mut builder| {
|
||||||
|
for variant_name in self.species.variant_paths.keys() {
|
||||||
|
let this = self.clone();
|
||||||
|
let variant_name = variant_name.to_string();
|
||||||
|
builder = builder.insert_fn(variant_name.clone(), move |selector| {
|
||||||
|
let svg = this.get_variant(&variant_name);
|
||||||
|
if let Some(svg) = svg {
|
||||||
|
if let Some(element) = query_selector(svg, &selector) {
|
||||||
|
if let Some(string) = xml_to_string(element) {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String::new()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
builder
|
||||||
|
});
|
||||||
|
|
||||||
|
for asset_name in self.species.asset_paths.keys() {
|
||||||
|
let this = self.clone();
|
||||||
|
let asset_name = asset_name.to_string();
|
||||||
|
|
||||||
|
builder = builder.insert_fn(asset_name.clone(), move |selector| {
|
||||||
|
let svg = this.get_asset(&asset_name);
|
||||||
|
if let Some(svg) = svg {
|
||||||
|
if let Some(element) = query_selector(svg, &selector) {
|
||||||
|
if let Some(string) = xml_to_string(element) {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String::new()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 -->")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
format!("<!-- Error in parsing xml: {} -->", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::from("<!-- Error in parsing color or element -->")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::from("<!-- Invalid syntax: expected `color|xml` -->")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
builder = builder.insert("vars", &self.species.vars).unwrap();
|
||||||
|
|
||||||
|
if include_parent {
|
||||||
|
let mut this = self.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
builder = builder.insert_map(&this.species.name, |_| {
|
||||||
|
this.get_builder(variant_name, false)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref parent) = this.parent {
|
||||||
|
this = *parent.clone();
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: memoize the builder to this stage
|
||||||
|
|
||||||
|
// Variant tags
|
||||||
|
if let Some(tags) = self.species.variants.get(variant_name) {
|
||||||
|
builder = builder.insert_map("tags", move |mut builder| {
|
||||||
|
for tag in tags.iter() {
|
||||||
|
builder = builder.insert_bool(tag, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_variant(&self, name: &String) -> Option<Element> {
|
||||||
|
let rendered = self.rendered_variants.lock().unwrap().get(name).cloned();
|
||||||
|
if let Some(rendered) = rendered {
|
||||||
|
Some(rendered)
|
||||||
|
} else if let Some(path) = self.species.variant_paths.get(name) {
|
||||||
|
// TODO: log error
|
||||||
|
let template = self.compile(path).ok()?;
|
||||||
|
let data = self.get_data(name);
|
||||||
|
let rendered = template.render_data_to_string(&data).ok()?;
|
||||||
|
|
||||||
|
let parsed = Element::parse(rendered.as_bytes()).ok()?;
|
||||||
|
self.rendered_variants
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(name.clone(), parsed.clone());
|
||||||
|
|
||||||
|
Some(parsed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_asset(&self, name: &String) -> Option<Element> {
|
||||||
|
let loaded = self.loaded_assets.lock().unwrap().get(name).cloned();
|
||||||
|
if let Some(loaded) = loaded {
|
||||||
|
Some(loaded)
|
||||||
|
} else if let Some(path) = self.species.asset_paths.get(name) {
|
||||||
|
let string = std::fs::read_to_string(path).ok()?;
|
||||||
|
let parsed = Element::parse(string.as_bytes()).ok()?;
|
||||||
|
self.loaded_assets
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(name.clone(), parsed.clone());
|
||||||
|
|
||||||
|
Some(parsed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn species(&self) -> Arc<SpeciesDecl> {
|
||||||
|
Arc::clone(&self.species)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialLoader for RenderingContext {
|
||||||
|
fn load(&self, name: impl AsRef<Path>) -> Result<String, mustache::Error> {
|
||||||
|
let name = name.as_ref().to_str().ok_or(mustache::Error::InvalidStr)?;
|
||||||
|
|
||||||
|
let components = name.split('.').collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if components.len() == 1 {
|
||||||
|
if let Some(path) = self.species.template_paths.get(name) {
|
||||||
|
Ok(std::fs::read_to_string(path)?)
|
||||||
|
} else {
|
||||||
|
eprintln!("No template named {}", name);
|
||||||
|
Err(mustache::Error::NoFilename)
|
||||||
|
}
|
||||||
|
} else if components.len() == 2 {
|
||||||
|
if components[0] == self.species.name {
|
||||||
|
self.load(components[1])
|
||||||
|
} else if let Some(ref parent) = self.parent {
|
||||||
|
parent.load(name)
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Cannot get template named {}: no species called {} in the inheritance tree",
|
||||||
|
name,
|
||||||
|
components[0]
|
||||||
|
);
|
||||||
|
Err(mustache::Error::NoFilename)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Cannot get template named {}: expected `name` or `species.name`", name);
|
||||||
|
Err(mustache::Error::NoFilename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_fill(color: &str, xml: &mut Element) {
|
||||||
|
// Substitute the fill color
|
||||||
|
if let Some(style) = xml.attributes.get("style") {
|
||||||
|
xml.attributes.insert(
|
||||||
|
"style".to_string(),
|
||||||
|
format!("{};fill: {};", style, color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(_fill) = xml.attributes.get("fill") {
|
||||||
|
xml.attributes.insert("fill".to_string(), color.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in xml.children.iter_mut() {
|
||||||
|
if let XMLNode::Element(ref mut child) = child {
|
||||||
|
set_fill(color, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_selector(svg: Element, pattern: &str) -> Option<Element> {
|
||||||
|
if pattern == "" {
|
||||||
|
return Some(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in svg.children {
|
||||||
|
if let XMLNode::Element(child) = child {
|
||||||
|
if let ("#", pattern_id) = pattern.split_at(1) {
|
||||||
|
if child
|
||||||
|
.attributes
|
||||||
|
.get("id")
|
||||||
|
.map(|id| id == pattern_id)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return Some(child);
|
||||||
|
} else if child.children.len() > 0 {
|
||||||
|
if let Some(res) = query_selector(child, pattern) {
|
||||||
|
return Some(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xml_to_string(element: Element) -> Option<String> {
|
||||||
|
let mut s: Vec<u8> = Vec::new();
|
||||||
|
let mut config = xmltree::EmitterConfig::default();
|
||||||
|
config.perform_indent = true;
|
||||||
|
config.write_document_declaration = false;
|
||||||
|
|
||||||
|
element.write_with_config(&mut s, config).ok()?;
|
||||||
|
|
||||||
|
String::from_utf8(s).ok()
|
||||||
|
}
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 5.1 KiB |