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 @@ + + LPKG Logo + Stylised package icon with circuitry and the letters LPKG. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LPKG + + + Lightweight Package Manager + + 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 @@ + + Nixette Logo + Wordmark combining Nix and Gentoo motifs with trans pride colours. + + + + + + + + + + + + + + + + + + + + + + + + + NIXETTE + + + Declarative · Sourceful · Herself + + 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 @@ + + Nixette Mascot Badge + Chibi penguin mascot with trans flag hair, blending Nix and Gentoo motifs. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NIXIE + + + Declarative · Sourceful · Herself + + 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 @@ + + Nixette Wallpaper + Gradient wallpaper combining trans flag waves with Nix and Gentoo motifs. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NIXETTE + + + Declarative · Sourceful · Herself + + 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!( - ""); - out.push('\n'); - - let (title_id, desc_id) = ( - aria_label - .as_ref() - .map(|ids| ids.0.as_str()) - .unwrap_or("title"), - aria_label - .as_ref() - .map(|ids| ids.1.as_str()) - .unwrap_or("desc"), - ); - - if let Some(title) = title { - out.push_str(&format!(" {}\n", title_id, title)); - } - if let Some(desc) = desc { - out.push_str(&format!(" {}\n", desc_id, desc)); - } - - if !defs.is_empty() { - out.push_str(" \n"); - for block in &defs { - out.push_str(block); - } - out.push_str(" \n"); - } - - for element in elements { - out.push_str(&element); - out.push('\n'); - } - - out.push_str("\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 = 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!( + ""); + out.push('\n'); + + if let Some(title) = title { + out.push_str(&format!(" {}\n", title_id, title)); + } + if let Some(desc) = desc { + out.push_str(&format!(" {}\n", desc_id, desc)); + } + + if !defs.is_empty() { + out.push_str(" \n"); + for block in &defs { + out.push_str(block); + } + out.push_str(" \n"); + } + + for element in &elements { + out.push_str(element); + out.push('\n'); + } + + out.push_str("\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 = 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() +}