diff --git a/assets/logo.svg b/assets/logo.svg
new file mode 100644
index 0000000..f6fa3ce
--- /dev/null
+++ b/assets/logo.svg
@@ -0,0 +1,53 @@
+
diff --git a/assets/nixette-logo.svg b/assets/nixette-logo.svg
new file mode 100644
index 0000000..27dbe2f
--- /dev/null
+++ b/assets/nixette-logo.svg
@@ -0,0 +1,33 @@
+
diff --git a/assets/nixette-mascot.svg b/assets/nixette-mascot.svg
new file mode 100644
index 0000000..c0ca461
--- /dev/null
+++ b/assets/nixette-mascot.svg
@@ -0,0 +1,50 @@
+
diff --git a/assets/nixette-wallpaper.svg b/assets/nixette-wallpaper.svg
new file mode 100644
index 0000000..a0eb1cf
--- /dev/null
+++ b/assets/nixette-wallpaper.svg
@@ -0,0 +1,42 @@
+
diff --git a/src/bin/logo_gen.rs b/src/bin/logo_gen.rs
index e85eda4..6b4715b 100644
--- a/src/bin/logo_gen.rs
+++ b/src/bin/logo_gen.rs
@@ -1,74 +1,52 @@
use anyhow::Result;
+use package_management::svg_builder::{Defs, Document, Element, Filter, Gradient, Group, path};
use std::fs;
fn main() -> Result<()> {
- let logo_svg = build_logo_svg();
+ let svg = build_logo_svg();
fs::create_dir_all("assets")?;
- fs::write("assets/logo.svg", logo_svg)?;
+ fs::write("assets/logo.svg", svg)?;
Ok(())
}
fn build_logo_svg() -> String {
- use svg::*;
+ let defs = Defs::new()
+ .linear_gradient(
+ "bgGradient",
+ Gradient::new("0", "0", "1", "1")
+ .stop("0%", &[("stop-color", "#0f172a")])
+ .stop("100%", &[("stop-color", "#1e293b")]),
+ )
+ .linear_gradient(
+ "cubeGradient",
+ Gradient::new("0", "0", "1", "1")
+ .stop("0%", &[("stop-color", "#38bdf8")])
+ .stop("100%", &[("stop-color", "#0ea5e9")]),
+ )
+ .linear_gradient(
+ "cubeShadow",
+ Gradient::new("0", "1", "1", "0")
+ .stop("0%", &[("stop-color", "#0ea5e9"), ("stop-opacity", "0.4")])
+ .stop("100%", &[("stop-color", "#38bdf8"), ("stop-opacity", "0.1")]),
+ )
+ .linear_gradient(
+ "textGradient",
+ Gradient::new("0", "0", "0", "1")
+ .stop("0%", &[("stop-color", "#f8fafc")])
+ .stop("100%", &[("stop-color", "#cbd5f5")]),
+ )
+ .filter(
+ "glow",
+ Filter::new()
+ .attr("x", "-20%")
+ .attr("y", "-20%")
+ .attr("width", "140%")
+ .attr("height", "140%")
+ .raw("")
+ .raw(""),
+ );
- let mut doc = Document::new(640, 320)
- .view_box("0 0 640 320")
- .role("img")
- .aria_label("title", "desc")
- .title("LPKG Logo")
- .desc("Stylised package icon with circuitry and the letters LPKG.");
-
- let mut defs = Defs::new();
- defs = defs.linear_gradient(
- "bgGradient",
- Gradient::new("0", "0", "1", "1")
- .stop("0%", &[("stop-color", "#0f172a")])
- .stop("100%", &[("stop-color", "#1e293b")]),
- );
- defs = defs.linear_gradient(
- "cubeGradient",
- Gradient::new("0", "0", "1", "1")
- .stop("0%", &[("stop-color", "#38bdf8")])
- .stop("100%", &[("stop-color", "#0ea5e9")]),
- );
- defs = defs.linear_gradient(
- "cubeShadow",
- Gradient::new("0", "1", "1", "0")
- .stop("0%", &[("stop-color", "#0ea5e9"), ("stop-opacity", "0.4")])
- .stop(
- "100%",
- &[("stop-color", "#38bdf8"), ("stop-opacity", "0.1")],
- ),
- );
- defs = defs.linear_gradient(
- "textGradient",
- Gradient::new("0", "0", "0", "1")
- .stop("0%", &[("stop-color", "#f8fafc")])
- .stop("100%", &[("stop-color", "#cbd5f5")]),
- );
- defs = defs.filter(
- "glow",
- Filter::new()
- .attr("x", "-20%")
- .attr("y", "-20%")
- .attr("width", "140%")
- .attr("height", "140%")
- .raw("")
- .raw(""),
- );
-
- doc = doc.add_defs(defs);
-
- doc = doc.add_element(
- Element::new("rect")
- .attr("width", "640")
- .attr("height", "320")
- .attr("rx", "28")
- .attr("fill", "url(#bgGradient)")
- .empty(),
- );
-
- let cube_group = Group::new()
+ let cube_inner = Group::new()
.attr("filter", "url(#glow)")
.child(
Element::new("path")
@@ -132,7 +110,10 @@ fn build_logo_svg() -> String {
.attr("fill", "#38bdf8")
.empty(),
);
- doc = doc.add_element(cube_group);
+
+ let cube = Group::new()
+ .attr("transform", "translate(100 60)")
+ .child(cube_inner);
let circuits = Group::new()
.attr("fill", "none")
@@ -146,9 +127,8 @@ fn build_logo_svg() -> String {
.child(path("M448 110h72"))
.child(path("M472 142h88"))
.child(path("M448 174h96"));
- doc = doc.add_element(circuits);
- let text_group = Group::new()
+ let title_text = Group::new()
.attr(
"font-family",
"'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
@@ -163,7 +143,6 @@ fn build_logo_svg() -> String {
.attr("fill", "url(#textGradient)")
.text("LPKG"),
);
- doc = doc.add_element(text_group);
let tagline_group = Group::new()
.attr(
@@ -178,345 +157,25 @@ fn build_logo_svg() -> String {
.attr("y", "278")
.text("Lightweight Package Manager"),
);
- doc = doc.add_element(tagline_group);
- doc.finish()
-}
-
-mod svg {
- #[derive(Default)]
- pub struct Document {
- width: u32,
- height: u32,
- view_box: Option,
- role: Option,
- aria_label: Option<(String, String)>,
- title: Option,
- desc: Option,
- defs: Vec,
- elements: Vec,
- }
-
- impl Document {
- pub fn new(width: u32, height: u32) -> Self {
- Self {
- width,
- height,
- ..Default::default()
- }
- }
-
- pub fn view_box(mut self, value: &str) -> Self {
- self.view_box = Some(value.to_string());
- self
- }
-
- pub fn role(mut self, value: &str) -> Self {
- self.role = Some(value.to_string());
- self
- }
-
- pub fn aria_label(mut self, title_id: &str, desc_id: &str) -> Self {
- self.aria_label = Some((title_id.to_string(), desc_id.to_string()));
- self
- }
-
- pub fn title(mut self, value: &str) -> Self {
- self.title = Some(value.to_string());
- self
- }
-
- pub fn desc(mut self, value: &str) -> Self {
- self.desc = Some(value.to_string());
- self
- }
-
- pub fn add_defs(mut self, defs: Defs) -> Self {
- self.defs.push(defs.finish());
- self
- }
-
- pub fn add_element(mut self, element: impl Into) -> Self {
- self.elements.push(element.into());
- self
- }
-
- pub fn finish(self) -> String {
- let Document {
- width,
- height,
- view_box,
- role,
- aria_label,
- title,
- desc,
- defs,
- elements,
- } = self;
-
- let mut out = String::new();
- out.push_str(&format!(
- "\n");
- out
- }
- }
-
- pub struct Defs {
- content: Vec,
- }
-
- impl Defs {
- pub fn new() -> Self {
- Self {
- content: Vec::new(),
- }
- }
-
- pub fn linear_gradient(mut self, id: &str, gradient: Gradient) -> Self {
- self.content.push(format!(" {}\n", gradient.render(id)));
- self
- }
-
- pub fn filter(mut self, id: &str, filter: Filter) -> Self {
- self.content.push(format!(" {}\n", filter.render(id)));
- self
- }
-
- pub fn finish(self) -> String {
- self.content.concat()
- }
- }
-
- pub struct Gradient {
- x1: String,
- y1: String,
- x2: String,
- y2: String,
- stops: Vec,
- }
-
- impl Gradient {
- pub fn new(x1: &str, y1: &str, x2: &str, y2: &str) -> Self {
- Self {
- x1: x1.to_string(),
- y1: y1.to_string(),
- x2: x2.to_string(),
- y2: y2.to_string(),
- stops: Vec::new(),
- }
- }
-
- pub fn stop(mut self, offset: &str, attrs: &[(&str, &str)]) -> Self {
- let mut tag = format!("");
- self.stops.push(tag);
- self
- }
-
- fn render(&self, id: &str) -> String {
- let mut out = format!(
- "\n",
- id, self.x1, self.y1, self.x2, self.y2
- );
- for stop in &self.stops {
- out.push_str(" ");
- out.push_str(stop);
- out.push('\n');
- }
- out.push_str(" ");
- out
- }
- }
-
- pub struct Filter {
- attrs: Vec<(String, String)>,
- content: Vec,
- }
-
- impl Filter {
- pub fn new() -> Self {
- Self {
- attrs: Vec::new(),
- content: Vec::new(),
- }
- }
-
- pub fn attr(mut self, key: &str, value: &str) -> Self {
- self.attrs.push((key.to_string(), value.to_string()));
- self
- }
-
- pub fn raw(mut self, markup: &str) -> Self {
- self.content.push(format!(" {}\n", markup));
- self
- }
-
- fn render(&self, id: &str) -> String {
- let attrs = self
- .attrs
- .iter()
- .map(|(k, v)| format!(" {}=\"{}\"", k, v))
- .collect::();
- let mut out = format!("\n", id, attrs);
- for child in &self.content {
- out.push_str(child);
- }
- out.push_str(" ");
- out
- }
- }
-
- pub struct Element {
- tag: String,
- attrs: Vec<(String, String)>,
- content: Option,
- }
-
- impl Element {
- pub fn new(tag: &str) -> Self {
- Self {
- tag: tag.to_string(),
- attrs: Vec::new(),
- content: None,
- }
- }
-
- pub fn attr(mut self, key: &str, value: &str) -> Self {
- self.attrs.push((key.to_string(), value.to_string()));
- self
- }
-
- pub fn text(mut self, text: &str) -> String {
- let content = format!("{}", text);
- self.content = Some(content);
- self.render()
- }
-
- pub fn empty(mut self) -> String {
- self.content = None;
- self.render()
- }
-
- fn render(&self) -> String {
- let attrs = self
- .attrs
- .iter()
- .map(|(k, v)| format!(" {}=\"{}\"", k, v))
- .collect::();
- if let Some(content) = &self.content {
- format!(
- " <{tag}{attrs}>{content}{tag}>",
- tag = self.tag,
- attrs = attrs,
- content = content
- )
- } else {
- format!(" <{tag}{attrs} />", tag = self.tag, attrs = attrs)
- }
- }
- }
-
- pub struct Group {
- attrs: Vec<(String, String)>,
- children: Vec,
- }
-
- impl Group {
- pub fn new() -> Self {
- Self {
- attrs: Vec::new(),
- children: Vec::new(),
- }
- }
-
- pub fn attr(mut self, key: &str, value: &str) -> Self {
- self.attrs.push((key.to_string(), value.to_string()));
- self
- }
-
- pub fn child(mut self, element: String) -> Self {
- self.children.push(element);
- self
- }
-
- pub fn render(&self) -> String {
- let attrs = self
- .attrs
- .iter()
- .map(|(k, v)| format!(" {}=\"{}\"", k, v))
- .collect::();
- let mut out = format!(" \n", attrs);
- for child in &self.children {
- out.push_str(child);
- out.push('\n');
- }
- out.push_str(" ");
- out
- }
- }
-
- impl From for String {
- fn from(group: Group) -> Self {
- group.render()
- }
- }
-
- impl From for String {
- fn from(element: Element) -> Self {
- element.render()
- }
- }
-
- pub fn path(d: &str) -> String {
- Element::new("path").attr("d", d).empty()
- }
+ Document::new(640, 320)
+ .view_box("0 0 640 320")
+ .role("img")
+ .aria_label("title", "desc")
+ .title("LPKG Logo")
+ .desc("Stylised package icon with circuitry and the letters LPKG.")
+ .add_defs(defs)
+ .add_element(
+ Element::new("rect")
+ .attr("width", "640")
+ .attr("height", "320")
+ .attr("rx", "28")
+ .attr("fill", "url(#bgGradient)")
+ .empty(),
+ )
+ .add_element(cube)
+ .add_element(circuits)
+ .add_element(title_text)
+ .add_element(tagline_group)
+ .finish()
}
diff --git a/src/bin/nixette_logo_gen.rs b/src/bin/nixette_logo_gen.rs
new file mode 100644
index 0000000..5f18f55
--- /dev/null
+++ b/src/bin/nixette_logo_gen.rs
@@ -0,0 +1,126 @@
+use anyhow::Result;
+use package_management::svg_builder::{Defs, Document, Element, Filter, Gradient, Group};
+use std::fs;
+
+fn main() -> Result<()> {
+ let svg = build_nixette_logo();
+ fs::create_dir_all("assets")?;
+ fs::write("assets/nixette-logo.svg", svg)?;
+ Ok(())
+}
+
+fn build_nixette_logo() -> String {
+ let defs = Defs::new()
+ .linear_gradient(
+ "bg",
+ Gradient::new("0", "0", "1", "1")
+ .stop("0%", &[("stop-color", "#55CDFC")])
+ .stop("100%", &[("stop-color", "#F7A8B8")]),
+ )
+ .linear_gradient(
+ "text",
+ Gradient::new("0", "0", "0", "1")
+ .stop("0%", &[("stop-color", "#FFFFFF")])
+ .stop("100%", &[("stop-color", "#E5E7FF")]),
+ )
+ .filter(
+ "softShadow",
+ Filter::new()
+ .attr("x", "-10%")
+ .attr("y", "-10%")
+ .attr("width", "120%")
+ .attr("height", "120%")
+ .raw(""),
+ );
+
+ let emblem = Group::new().attr("transform", "translate(100 60)").child(
+ Group::new()
+ .attr("filter", "url(#softShadow)")
+ .child(
+ Element::new("path")
+ .attr("d", "M40 40 L72 0 L144 0 L176 40 L144 80 L72 80 Z")
+ .attr("fill", "url(#bg)")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M72 0 L144 80")
+ .attr("stroke", "#FFFFFF")
+ .attr("stroke-width", "6")
+ .attr("stroke-linecap", "round")
+ .attr("opacity", "0.55")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M144 0 L72 80")
+ .attr("stroke", "#FFFFFF")
+ .attr("stroke-width", "6")
+ .attr("stroke-linecap", "round")
+ .attr("opacity", "0.55")
+ .empty(),
+ )
+ .child(
+ Element::new("circle")
+ .attr("cx", "108")
+ .attr("cy", "40")
+ .attr("r", "22")
+ .attr("fill", "#0F172A")
+ .attr("stroke", "#FFFFFF")
+ .attr("stroke-width", "6")
+ .attr("opacity", "0.85")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M108 24c8 0 14 6 14 16s-6 16-14 16")
+ .attr("stroke", "#F7A8B8")
+ .attr("stroke-width", "4")
+ .attr("stroke-linecap", "round")
+ .attr("fill", "none")
+ .empty(),
+ ),
+ );
+
+ let wordmark = Group::new()
+ .attr("transform", "translate(220 126)")
+ .attr(
+ "font-family",
+ "'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
+ )
+ .attr("font-weight", "700")
+ .attr("font-size", "72")
+ .attr("letter-spacing", "4")
+ .attr("fill", "url(#text)")
+ .child(Element::new("text").text("NIXETTE"));
+
+ let subtitle = Group::new()
+ .attr("transform", "translate(220 160)")
+ .attr(
+ "font-family",
+ "'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
+ )
+ .attr("font-size", "22")
+ .attr("fill", "#A5B4FC")
+ .child(Element::new("text").text("Declarative · Sourceful · Herself"));
+
+ Document::new(640, 200)
+ .view_box("0 0 640 200")
+ .role("img")
+ .aria_label("title", "desc")
+ .title("Nixette Logo")
+ .desc("Wordmark combining Nix and Gentoo motifs with trans pride colours.")
+ .add_defs(defs)
+ .add_element(
+ Element::new("rect")
+ .attr("width", "640")
+ .attr("height", "200")
+ .attr("rx", "36")
+ .attr("fill", "#0F172A")
+ .empty(),
+ )
+ .add_element(emblem)
+ .add_element(wordmark)
+ .add_element(subtitle)
+ .finish()
+}
diff --git a/src/bin/nixette_mascot_gen.rs b/src/bin/nixette_mascot_gen.rs
new file mode 100644
index 0000000..b07edd1
--- /dev/null
+++ b/src/bin/nixette_mascot_gen.rs
@@ -0,0 +1,170 @@
+use anyhow::Result;
+use package_management::svg_builder::{Defs, Document, Element, Gradient, Group};
+use std::fs;
+
+fn main() -> Result<()> {
+ let svg = build_mascot_svg();
+ fs::create_dir_all("assets")?;
+ fs::write("assets/nixette-mascot.svg", svg)?;
+ Ok(())
+}
+
+fn build_mascot_svg() -> String {
+ let defs = Defs::new()
+ .linear_gradient(
+ "bgGrad",
+ Gradient::new("0", "0", "0", "1")
+ .stop("0%", &[("stop-color", "#312E81")])
+ .stop("100%", &[("stop-color", "#1E1B4B")]),
+ )
+ .linear_gradient(
+ "hairLeft",
+ Gradient::new("0", "0", "1", "1")
+ .stop("0%", &[("stop-color", "#55CDFC")])
+ .stop("100%", &[("stop-color", "#0EA5E9")]),
+ )
+ .linear_gradient(
+ "hairRight",
+ Gradient::new("1", "0", "0", "1")
+ .stop("0%", &[("stop-color", "#F7A8B8")])
+ .stop("100%", &[("stop-color", "#FB7185")]),
+ )
+ .linear_gradient(
+ "bellyGrad",
+ Gradient::new("0", "0", "0", "1")
+ .stop("0%", &[("stop-color", "#FFFFFF")])
+ .stop("100%", &[("stop-color", "#E2E8F0")]),
+ );
+
+ let body = Group::new()
+ .attr("transform", "translate(240 220)")
+ .child(
+ Element::new("path")
+ .attr("d", "M-160 -20 C-140 -160 140 -160 160 -20 C180 140 60 220 0 220 C-60 220 -180 140 -160 -20")
+ .attr("fill", "#0F172A")
+ .empty(),
+ )
+ .child(
+ Element::new("ellipse")
+ .attr("cx", "0")
+ .attr("cy", "40")
+ .attr("rx", "120")
+ .attr("ry", "140")
+ .attr("fill", "#1E293B")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M-88 -80 Q-40 -140 0 -120 Q40 -140 88 -80")
+ .attr("fill", "#1E293B")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M-96 -84 Q-60 -160 -8 -132 L-8 -40 Z")
+ .attr("fill", "url(#hairLeft)")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M96 -84 Q60 -160 8 -132 L8 -40 Z")
+ .attr("fill", "url(#hairRight)")
+ .empty(),
+ )
+ .child(ellipse(-44.0, -8.0, 26.0, 32.0, "#FFFFFF"))
+ .child(ellipse(44.0, -8.0, 26.0, 32.0, "#FFFFFF"))
+ .child(circle(-44.0, -4.0, 14.0, "#0F172A"))
+ .child(circle(44.0, -4.0, 14.0, "#0F172A"))
+ .child(circle_with_opacity(-40.0, -8.0, 6.0, "#FFFFFF", 0.7))
+ .child(circle_with_opacity(48.0, -10.0, 6.0, "#FFFFFF", 0.7))
+ .child(path_with_fill("M0 12 L-18 32 Q0 44 18 32 Z", "#F472B6"))
+ .child(path_with_fill("M0 32 L-16 52 Q0 60 16 52 Z", "#FBEAED"))
+ .child(path_with_fill("M0 46 Q-32 78 0 86 Q32 78 0 46", "#FCA5A5"))
+ .child(
+ Element::new("ellipse")
+ .attr("cx", "0")
+ .attr("cy", "74")
+ .attr("rx", "70")
+ .attr("ry", "82")
+ .attr("fill", "url(#bellyGrad)")
+ .empty(),
+ )
+ .child(path_with_fill("M-128 48 Q-176 56 -176 120 Q-128 112 -104 80", "#F7A8B8"))
+ .child(path_with_fill("M128 48 Q176 56 176 120 Q128 112 104 80", "#55CDFC"))
+ .child(circle_with_opacity(-100.0, 94.0, 18.0, "#FDE68A", 0.85))
+ .child(circle_with_opacity(100.0, 94.0, 18.0, "#FDE68A", 0.85));
+
+ Document::new(480, 520)
+ .view_box("0 0 480 520")
+ .role("img")
+ .aria_label("title", "desc")
+ .title("Nixette Mascot Badge")
+ .desc("Chibi penguin mascot with trans flag hair, blending Nix and Gentoo motifs.")
+ .add_defs(defs)
+ .add_element(
+ Element::new("rect")
+ .attr("width", "480")
+ .attr("height", "520")
+ .attr("rx", "48")
+ .attr("fill", "url(#bgGrad)")
+ .empty(),
+ )
+ .add_element(body)
+ .add_element(
+ Group::new()
+ .attr("transform", "translate(90 420)")
+ .attr(
+ "font-family",
+ "'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
+ )
+ .attr("font-size", "42")
+ .attr("fill", "#E0E7FF")
+ .attr("letter-spacing", "6")
+ .child(Element::new("text").text("NIXIE")),
+ )
+ .add_element(
+ Group::new()
+ .attr("transform", "translate(90 468)")
+ .attr(
+ "font-family",
+ "'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
+ )
+ .attr("font-size", "20")
+ .attr("fill", "#A5B4FC")
+ .child(Element::new("text").text("Declarative · Sourceful · Herself")),
+ )
+ .finish()
+}
+
+fn ellipse(cx: f64, cy: f64, rx: f64, ry: f64, fill: &str) -> String {
+ Element::new("ellipse")
+ .attr("cx", &format!("{}", cx))
+ .attr("cy", &format!("{}", cy))
+ .attr("rx", &format!("{}", rx))
+ .attr("ry", &format!("{}", ry))
+ .attr("fill", fill)
+ .empty()
+}
+
+fn circle(cx: f64, cy: f64, r: f64, fill: &str) -> String {
+ Element::new("circle")
+ .attr("cx", &format!("{}", cx))
+ .attr("cy", &format!("{}", cy))
+ .attr("r", &format!("{}", r))
+ .attr("fill", fill)
+ .empty()
+}
+
+fn circle_with_opacity(cx: f64, cy: f64, r: f64, fill: &str, opacity: f64) -> String {
+ Element::new("circle")
+ .attr("cx", &format!("{}", cx))
+ .attr("cy", &format!("{}", cy))
+ .attr("r", &format!("{}", r))
+ .attr("fill", fill)
+ .attr("opacity", &format!("{}", opacity))
+ .empty()
+}
+
+fn path_with_fill(d: &str, fill: &str) -> String {
+ Element::new("path").attr("d", d).attr("fill", fill).empty()
+}
diff --git a/src/bin/nixette_wallpaper_gen.rs b/src/bin/nixette_wallpaper_gen.rs
new file mode 100644
index 0000000..225f157
--- /dev/null
+++ b/src/bin/nixette_wallpaper_gen.rs
@@ -0,0 +1,128 @@
+use anyhow::Result;
+use package_management::svg_builder::{
+ Defs, Document, Element, Gradient, Group, RadialGradient, path,
+};
+use std::fs;
+
+fn main() -> Result<()> {
+ let svg = build_wallpaper_svg();
+ fs::create_dir_all("assets")?;
+ fs::write("assets/nixette-wallpaper.svg", svg)?;
+ Ok(())
+}
+
+fn build_wallpaper_svg() -> String {
+ let defs = Defs::new()
+ .linear_gradient(
+ "sky",
+ Gradient::new("0", "0", "1", "1")
+ .stop("0%", &[("stop-color", "#0f172a")])
+ .stop("100%", &[("stop-color", "#1e1b4b")]),
+ )
+ .linear_gradient(
+ "wave1",
+ Gradient::new("0", "0", "1", "0")
+ .stop("0%", &[("stop-color", "#55CDFC"), ("stop-opacity", "0")])
+ .stop("50%", &[("stop-color", "#55CDFC"), ("stop-opacity", "0.5")])
+ .stop("100%", &[("stop-color", "#55CDFC"), ("stop-opacity", "0")]),
+ )
+ .linear_gradient(
+ "wave2",
+ Gradient::new("1", "0", "0", "0")
+ .stop("0%", &[("stop-color", "#F7A8B8"), ("stop-opacity", "0")])
+ .stop(
+ "50%",
+ &[("stop-color", "#F7A8B8"), ("stop-opacity", "0.55")],
+ )
+ .stop("100%", &[("stop-color", "#F7A8B8"), ("stop-opacity", "0")]),
+ )
+ .radial_gradient(
+ "halo",
+ RadialGradient::new("0.5", "0.5", "0.7")
+ .stop("0%", &[("stop-color", "#FDE68A"), ("stop-opacity", "0.8")])
+ .stop("100%", &[("stop-color", "#FDE68A"), ("stop-opacity", "0")]),
+ );
+
+ let text = Group::new()
+ .attr("transform", "translate(940 1320)")
+ .attr(
+ "font-family",
+ "'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
+ )
+ .attr("font-size", "220")
+ .attr("font-weight", "700")
+ .attr("letter-spacing", "18")
+ .attr("fill", "#FFFFFF")
+ .attr("opacity", "0.95")
+ .child(Element::new("text").text("NIXETTE"));
+
+ let subtitle = Group::new()
+ .attr("transform", "translate(960 1500)")
+ .attr(
+ "font-family",
+ "'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
+ )
+ .attr("font-size", "64")
+ .attr("fill", "#F7A8B8")
+ .attr("opacity", "0.9")
+ .child(Element::new("text").text("Declarative · Sourceful · Herself"));
+
+ Document::new(3840, 2160)
+ .view_box("0 0 3840 2160")
+ .role("img")
+ .aria_label("title", "desc")
+ .title("Nixette Wallpaper")
+ .desc("Gradient wallpaper combining trans flag waves with Nix and Gentoo motifs.")
+ .add_defs(defs)
+ .add_element(
+ Element::new("rect")
+ .attr("width", "3840")
+ .attr("height", "2160")
+ .attr("fill", "url(#sky)")
+ .empty(),
+ )
+ .add_element(
+ Element::new("rect")
+ .attr("x", "0")
+ .attr("y", "0")
+ .attr("width", "3840")
+ .attr("height", "2160")
+ .attr("fill", "url(#halo)")
+ .attr("opacity", "0.4")
+ .empty(),
+ )
+ .add_element(
+ Element::new("path")
+ .attr("d", "M0 1430 C640 1320 1280 1580 1860 1500 C2440 1420 3040 1660 3840 1500 L3840 2160 L0 2160 Z")
+ .attr("fill", "url(#wave1)")
+ .empty(),
+ )
+ .add_element(
+ Element::new("path")
+ .attr("d", "M0 1700 C500 1580 1200 1880 1900 1760 C2600 1640 3200 1920 3840 1800 L3840 2160 L0 2160 Z")
+ .attr("fill", "url(#wave2)")
+ .empty(),
+ )
+ .add_element(
+ Group::new()
+ .attr("opacity", "0.08")
+ .attr("fill", "none")
+ .attr("stroke", "#FFFFFF")
+ .attr("stroke-width", "24")
+ .child(path("M600 360 l220 -220 h360 l220 220 l-220 220 h-360 z"))
+ .child(path("M600 360 l220 -220"))
+ .child(path("M820 140 l220 220")),
+ )
+ .add_element(
+ Group::new()
+ .attr("opacity", "0.12")
+ .attr("fill", "none")
+ .attr("stroke", "#FFFFFF")
+ .attr("stroke-width", "22")
+ .attr("transform", "translate(2820 320) scale(0.9)")
+ .child(path("M0 0 C120 -40 220 40 220 160 C220 260 160 320 60 320")),
+ )
+ .add_element(text)
+ .add_element(subtitle)
+ .finish()
+}
diff --git a/src/lib.rs b/src/lib.rs
index 732597d..033ece4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,6 +7,7 @@ pub mod ingest;
pub mod md5_utils;
pub mod mirrors;
pub mod pkgs;
+pub mod svg_builder;
pub mod version_check;
pub mod wget_list;
diff --git a/src/svg_builder.rs b/src/svg_builder.rs
new file mode 100644
index 0000000..77a6a74
--- /dev/null
+++ b/src/svg_builder.rs
@@ -0,0 +1,375 @@
+#[derive(Default)]
+pub struct Document {
+ width: u32,
+ height: u32,
+ view_box: Option,
+ role: Option,
+ aria_label: Option<(String, String)>,
+ title: Option,
+ desc: Option,
+ defs: Vec,
+ elements: Vec,
+}
+
+impl Document {
+ pub fn new(width: u32, height: u32) -> Self {
+ Self {
+ width,
+ height,
+ ..Default::default()
+ }
+ }
+
+ pub fn view_box(mut self, value: &str) -> Self {
+ self.view_box = Some(value.to_string());
+ self
+ }
+
+ pub fn role(mut self, value: &str) -> Self {
+ self.role = Some(value.to_string());
+ self
+ }
+
+ pub fn aria_label(mut self, title_id: &str, desc_id: &str) -> Self {
+ self.aria_label = Some((title_id.to_string(), desc_id.to_string()));
+ self
+ }
+
+ pub fn title(mut self, value: &str) -> Self {
+ self.title = Some(value.to_string());
+ self
+ }
+
+ pub fn desc(mut self, value: &str) -> Self {
+ self.desc = Some(value.to_string());
+ self
+ }
+
+ pub fn add_defs(mut self, defs: Defs) -> Self {
+ self.defs.push(defs.finish());
+ self
+ }
+
+ pub fn add_element(mut self, element: impl Into) -> Self {
+ self.elements.push(element.into());
+ self
+ }
+
+ pub fn finish(self) -> String {
+ let Document {
+ width,
+ height,
+ view_box,
+ role,
+ aria_label,
+ title,
+ desc,
+ defs,
+ elements,
+ } = self;
+
+ let mut out = String::new();
+ out.push_str(&format!(
+ "\n");
+ out
+ }
+}
+
+pub struct Defs {
+ content: Vec,
+}
+
+impl Defs {
+ pub fn new() -> Self {
+ Self {
+ content: Vec::new(),
+ }
+ }
+
+ pub fn linear_gradient(mut self, id: &str, gradient: Gradient) -> Self {
+ self.content
+ .push(format!(" {}\n", gradient.render_linear(id)));
+ self
+ }
+
+ pub fn radial_gradient(mut self, id: &str, gradient: RadialGradient) -> Self {
+ self.content.push(format!(" {}\n", gradient.render(id)));
+ self
+ }
+
+ pub fn filter(mut self, id: &str, filter: Filter) -> Self {
+ self.content.push(format!(" {}\n", filter.render(id)));
+ self
+ }
+
+ pub fn finish(self) -> String {
+ self.content.concat()
+ }
+}
+
+pub struct Gradient {
+ x1: String,
+ y1: String,
+ x2: String,
+ y2: String,
+ stops: Vec,
+}
+
+impl Gradient {
+ pub fn new(x1: &str, y1: &str, x2: &str, y2: &str) -> Self {
+ Self {
+ x1: x1.to_string(),
+ y1: y1.to_string(),
+ x2: x2.to_string(),
+ y2: y2.to_string(),
+ stops: Vec::new(),
+ }
+ }
+
+ pub fn stop(mut self, offset: &str, attrs: &[(&str, &str)]) -> Self {
+ let mut tag = format!("");
+ self.stops.push(tag);
+ self
+ }
+
+ fn render_linear(&self, id: &str) -> String {
+ let mut out = format!(
+ "\n",
+ id, self.x1, self.y1, self.x2, self.y2
+ );
+ for stop in &self.stops {
+ out.push_str(" ");
+ out.push_str(stop);
+ out.push('\n');
+ }
+ out.push_str(" ");
+ out
+ }
+}
+
+pub struct RadialGradient {
+ cx: String,
+ cy: String,
+ r: String,
+ stops: Vec,
+}
+
+impl RadialGradient {
+ pub fn new(cx: &str, cy: &str, r: &str) -> Self {
+ Self {
+ cx: cx.to_string(),
+ cy: cy.to_string(),
+ r: r.to_string(),
+ stops: Vec::new(),
+ }
+ }
+
+ pub fn stop(mut self, offset: &str, attrs: &[(&str, &str)]) -> Self {
+ let mut tag = format!("");
+ self.stops.push(tag);
+ self
+ }
+
+ fn render(&self, id: &str) -> String {
+ let mut out = format!(
+ "\n",
+ id, self.cx, self.cy, self.r
+ );
+ for stop in &self.stops {
+ out.push_str(" ");
+ out.push_str(stop);
+ out.push('\n');
+ }
+ out.push_str(" ");
+ out
+ }
+}
+
+pub struct Filter {
+ attrs: Vec<(String, String)>,
+ content: Vec,
+}
+
+impl Filter {
+ pub fn new() -> Self {
+ Self {
+ attrs: Vec::new(),
+ content: Vec::new(),
+ }
+ }
+
+ pub fn attr(mut self, key: &str, value: &str) -> Self {
+ self.attrs.push((key.to_string(), value.to_string()));
+ self
+ }
+
+ pub fn raw(mut self, markup: &str) -> Self {
+ self.content.push(format!(" {}\n", markup));
+ self
+ }
+
+ fn render(&self, id: &str) -> String {
+ let attrs = self
+ .attrs
+ .iter()
+ .map(|(k, v)| format!(" {}=\"{}\"", k, v))
+ .collect::();
+ let mut out = format!("\n", id, attrs);
+ for child in &self.content {
+ out.push_str(child);
+ }
+ out.push_str(" ");
+ out
+ }
+}
+
+pub struct Element {
+ tag: String,
+ attrs: Vec<(String, String)>,
+ content: Option,
+}
+
+impl Element {
+ pub fn new(tag: &str) -> Self {
+ Self {
+ tag: tag.to_string(),
+ attrs: Vec::new(),
+ content: None,
+ }
+ }
+
+ pub fn attr(mut self, key: &str, value: &str) -> Self {
+ self.attrs.push((key.to_string(), value.to_string()));
+ self
+ }
+
+ pub fn text(mut self, text: &str) -> String {
+ self.content = Some(text.to_string());
+ self.render()
+ }
+
+ pub fn empty(mut self) -> String {
+ self.content = None;
+ self.render()
+ }
+
+ fn render(&self) -> String {
+ let attrs = self
+ .attrs
+ .iter()
+ .map(|(k, v)| format!(" {}=\"{}\"", k, v))
+ .collect::();
+ if let Some(content) = &self.content {
+ format!(
+ " <{tag}{attrs}>{content}{tag}>",
+ tag = self.tag,
+ attrs = attrs,
+ content = content
+ )
+ } else {
+ format!(" <{tag}{attrs} />", tag = self.tag, attrs = attrs)
+ }
+ }
+}
+
+pub struct Group {
+ attrs: Vec<(String, String)>,
+ children: Vec,
+}
+
+impl Group {
+ pub fn new() -> Self {
+ Self {
+ attrs: Vec::new(),
+ children: Vec::new(),
+ }
+ }
+
+ pub fn attr(mut self, key: &str, value: &str) -> Self {
+ self.attrs.push((key.to_string(), value.to_string()));
+ self
+ }
+
+ pub fn child(mut self, element: impl Into) -> Self {
+ self.children.push(element.into());
+ self
+ }
+
+ pub fn render(&self) -> String {
+ let attrs = self
+ .attrs
+ .iter()
+ .map(|(k, v)| format!(" {}=\"{}\"", k, v))
+ .collect::();
+ let mut out = format!(" \n", attrs);
+ for child in &self.children {
+ out.push_str(child);
+ out.push('\n');
+ }
+ out.push_str(" ");
+ out
+ }
+}
+
+impl From for String {
+ fn from(group: Group) -> Self {
+ group.render()
+ }
+}
+
+impl From for String {
+ fn from(element: Element) -> Self {
+ element.render()
+ }
+}
+
+pub fn path(d: &str) -> String {
+ Element::new("path").attr("d", d).empty()
+}