diff --git a/.gitignore b/.gitignore index ea8c4bf..8ffdea6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ /target +shapemaker + +.vscode +test.svg \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 04dec81..f79c807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,58 +2,12 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ahash" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" -dependencies = [ - "const-random", -] - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chumsky" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d02796e4586c6c41aeb68eae9bfb4558a522c35f1430c14b40136c3706e09e4" -dependencies = [ - "ahash", -] - -[[package]] -name = "const-random" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" -dependencies = [ - "const-random-macro", - "proc-macro-hack", -] - -[[package]] -name = "const-random-macro" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" -dependencies = [ - "getrandom", - "once_cell", - "proc-macro-hack", - "tiny-keccak", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - [[package]] name = "docopt" version = "1.1.1" @@ -90,16 +44,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] -name = "once_cell" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" +name = "ppv-lite86" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" @@ -119,6 +67,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.7.0" @@ -156,10 +134,10 @@ dependencies = [ [[package]] name = "shapemaker" -version = "0.1.0" +version = "1.0.0" dependencies = [ - "chumsky", "docopt", + "rand", "serde", "svg", ] @@ -172,9 +150,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "svg" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e6ff893392e6a1eb94a210562432c6380cebf09d30962a012a655f7dde2ff8" +checksum = "e715e0c3fc987f4c435dc7189641fd9caa6919a74675ace605c38e201d278001" [[package]] name = "syn" @@ -187,15 +165,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "unicode-ident" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index 04d9361..cf4761b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "shapemaker" -version = "0.1.0" +version = "1.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chumsky = "0.8.0" docopt = "1.1.1" +rand = "0.8.5" serde = "1.0.147" -svg = "0.12.1" +svg = "0.13.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fe250c --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# shapemaker + +An experiment into the generation of 2D flat design abstract artwork using limited shape and color combinations, arranged in a 8-point grid. + +## Gallery + +
+ +**HAL 9000** +![HAL 9000](gallery/HAL-9000.svg) + +**Amogus** +![Amogus](gallery/amogus.svg) + +**Capitalism** +![Capitalism](gallery/capitalism.svg) + +**Cutting An Egg** +![Cutting An Egg](gallery/cutting-an-egg.svg) + +**Designing A Font** +![Designing A Font](gallery/designing-a-font.svg) + +**Drone Operating System** +![Drone Operating System](gallery/drone-operating-system.svg) + +**Iron Factory** +![Iron Factory](gallery/iron-factory.svg) + +**Japan Sledding Olympics** +![Japan Sledding Olympics](gallery/japan-sledding-olympics.svg) + +**Lunatic Green Energy** +![Lunatic Green Energy](gallery/lunatic-green-energy.svg) + +**Measuring Spirits** +![Measuring Spirits](gallery/measuring-spirits.svg) + +**Phone Cameras** +![Phone Cameras](gallery/phone-cameras.svg) + +**Reflections** +![Reflections](gallery/reflections.svg) + +**Spline Optimisation** +![Spline Optimisation](gallery/spline-optimisation.svg) + +**Tropical Fish** +![Tropical Fish](gallery/tropical-fish.svg) + +**Weaving** +![Weaving](gallery/weaving.svg) + + + +
diff --git a/README.md.in b/README.md.in new file mode 100644 index 0000000..bcf1614 --- /dev/null +++ b/README.md.in @@ -0,0 +1,11 @@ +# shapemaker + +An experiment into the generation of 2D flat design abstract artwork using limited shape and color combinations, arranged in a 8-point grid. + +## Gallery + +
+ +%gallery% + +
diff --git a/fill-gallery.rb b/fill-gallery.rb new file mode 100755 index 0000000..551343b --- /dev/null +++ b/fill-gallery.rb @@ -0,0 +1,20 @@ +#!/usr/bin/env ruby + +gallery = "" + +Dir.glob("gallery/*.svg").each do |file| + if file == "gallery/test.svg" then next end + + title = file + .sub(/^gallery\//, "") + .sub(/\.svg$/, "") + .gsub(/-/, " ").split(" ") + .map { |word| word.upcase == word ? word : word.capitalize } + .join(" ") + + gallery += "**#{title}**\n![#{title}](#{file})\n\n" +end + +File.open "README.md", "w" do |f| + f.write File.read("README.md.in").gsub("%gallery%", gallery) +end diff --git a/gallery/HAL-9000.svg b/gallery/HAL-9000.svg new file mode 100644 index 0000000..941e6b9 --- /dev/null +++ b/gallery/HAL-9000.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/amogus.svg b/gallery/amogus.svg new file mode 100644 index 0000000..509afdf --- /dev/null +++ b/gallery/amogus.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/capitalism.svg b/gallery/capitalism.svg new file mode 100644 index 0000000..ce05e26 --- /dev/null +++ b/gallery/capitalism.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/cutting-an-egg.svg b/gallery/cutting-an-egg.svg new file mode 100644 index 0000000..a4450b2 --- /dev/null +++ b/gallery/cutting-an-egg.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/designing-a-font.svg b/gallery/designing-a-font.svg new file mode 100644 index 0000000..3f76a7b --- /dev/null +++ b/gallery/designing-a-font.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/drone-operating-system.svg b/gallery/drone-operating-system.svg new file mode 100644 index 0000000..8b63b4d --- /dev/null +++ b/gallery/drone-operating-system.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/iron-factory.svg b/gallery/iron-factory.svg new file mode 100644 index 0000000..58dee5a --- /dev/null +++ b/gallery/iron-factory.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/japan-sledding-olympics.svg b/gallery/japan-sledding-olympics.svg new file mode 100644 index 0000000..ac42183 --- /dev/null +++ b/gallery/japan-sledding-olympics.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/lunatic-green-energy.svg b/gallery/lunatic-green-energy.svg new file mode 100644 index 0000000..496c059 --- /dev/null +++ b/gallery/lunatic-green-energy.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/measuring-spirits.svg b/gallery/measuring-spirits.svg new file mode 100644 index 0000000..3f85061 --- /dev/null +++ b/gallery/measuring-spirits.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/phone-cameras.svg b/gallery/phone-cameras.svg new file mode 100644 index 0000000..ea21c15 --- /dev/null +++ b/gallery/phone-cameras.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/reflections.svg b/gallery/reflections.svg new file mode 100644 index 0000000..d3caeee --- /dev/null +++ b/gallery/reflections.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/spline-optimisation.svg b/gallery/spline-optimisation.svg new file mode 100644 index 0000000..02b0b9d --- /dev/null +++ b/gallery/spline-optimisation.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/test.svg b/gallery/test.svg new file mode 100644 index 0000000..aaf96c5 --- /dev/null +++ b/gallery/test.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/tropical-fish.svg b/gallery/tropical-fish.svg new file mode 100644 index 0000000..c11f39f --- /dev/null +++ b/gallery/tropical-fish.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/weaving.svg b/gallery/weaving.svg new file mode 100644 index 0000000..2f60a2d --- /dev/null +++ b/gallery/weaving.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 0342476..182aa5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ -use std::collections::HashMap; +use std::{collections::HashMap, borrow::Borrow}; -use chumsky::prelude::*; -use chumsky::text; -use chumsky::text::{newline, whitespace}; use docopt::Docopt; +use rand::Rng; use serde::Deserialize; const USAGE: &'static str = " @@ -11,51 +9,223 @@ const USAGE: &'static str = " █░▄▄█░████░▄▄▀█▀▄▄▀█░▄▄█░▄▀▄░█░▄▄▀█░█▀█░▄▄█░▄▄▀█ █▄▄▀█░▄▄░█░▀▀░█░▀▀░█░▄▄█░█▄█░█░▀▀░█░▄▀█░▄▄█░▀▀▄█ █▄▄▄█▄██▄█▄██▄█░████▄▄▄█▄███▄█▄██▄█▄█▄█▄▄▄█▄█▄▄█ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀vVERSION▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Usage: shapemaker [options] - shapemaker (-h | --help) + shapemaker --help + shapemaker --version "; #[derive(Debug, Deserialize)] struct Args { arg_file: String, - flag_verbose: bool, + flag_version: bool, } fn main() { - let args: Args = Docopt::new(USAGE) + let args: Args = Docopt::new(USAGE.replace("VERSION", env!("CARGO_PKG_VERSION"))) .and_then(|d| d.deserialize()) .unwrap_or_else(|e| e.exit()); - let file_contents = std::fs::read_to_string(&args.arg_file).unwrap(); - let shapes: Vec = match parser().parse(file_contents) { - Ok(shapes) => { - println!("Parsed shapes: {:#?}", shapes); - shapes - } - Err(e) => { - println!("Error: {:?}", e); - std::process::exit(1); - } - }; + if args.flag_version { + println!("shapemaker {}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + } + + let shape = random_shape("test"); + + std::fs::write(shape.name.to_owned() + ".svg", shape.render()); + + // let file_contents = std::fs::read_to_string(&args.arg_file).unwrap(); + // let shape: Vec = match parser().parse(file_contents) { + // Ok(shape) => { + // println!("Parsed shape: {:#?}", shape); + // shape + // } + // Err(e) => { + // println!("Error: {:?}", e); + // std::process::exit(1); + // } + // }; +} + +fn random_shape(name: &'static str) -> Shape { + let mut objects: Vec<(Object, Option)> = vec![]; + let number_of_objects = rand::thread_rng().gen_range(3..7); + for _ in 0..number_of_objects { + let object = random_object(); + objects.push(( + object, + if rand::thread_rng().gen_bool(0.5) { + Some(random_fill()) + } else { + None + }, + )); + } + Shape { name, objects } +} + +fn random_object() -> Object { + let start = random_anchor(); + match rand::thread_rng().gen_range(1..=7) { + 1 => random_polygon(), + 2 => Object::BigCircle(random_center_anchor()), + 3 => Object::SmallCircle(start), + 4 => Object::Dot(start), + 5 => Object::CurveInward(start, random_end_anchor(start)), + 6 => Object::CurveOutward(start, random_end_anchor(start)), + 7 => Object::Line(random_anchor(), random_anchor()), + _ => unreachable!(), + } +} + +fn random_end_anchor(start: Anchor) -> Anchor { + match start { + Anchor::TopLeft => match rand::thread_rng().gen_range(1..=2) { + 1 => Anchor::Center, + 2 => Anchor::BottomRight, + _ => unreachable!(), + }, + Anchor::Top => match rand::thread_rng().gen_range(1..=2) { + 1 => Anchor::Left, + 2 => Anchor::Right, + _ => unreachable!(), + }, + Anchor::TopRight => match rand::thread_rng().gen_range(1..=2) { + 1 => Anchor::Center, + 2 => Anchor::BottomLeft, + _ => unreachable!(), + }, + Anchor::Left => match rand::thread_rng().gen_range(1..=2) { + 1 => Anchor::Top, + 2 => Anchor::Bottom, + _ => unreachable!(), + }, + Anchor::Center => match rand::thread_rng().gen_range(1..=4) { + 1 => Anchor::TopLeft, + 2 => Anchor::TopRight, + 3 => Anchor::BottomLeft, + 4 => Anchor::BottomRight, + _ => unreachable!(), + }, + Anchor::Right => match rand::thread_rng().gen_range(1..=2) { + 1 => Anchor::Top, + 2 => Anchor::Bottom, + _ => unreachable!(), + }, + Anchor::BottomLeft => match rand::thread_rng().gen_range(1..=2) { + 1 => Anchor::Center, + 2 => Anchor::TopRight, + _ => unreachable!(), + }, + Anchor::Bottom => match rand::thread_rng().gen_range(1..=2) { + 1 => Anchor::Left, + 2 => Anchor::Right, + _ => unreachable!(), + }, + Anchor::BottomRight => match rand::thread_rng().gen_range(1..=2) { + 1 => Anchor::Center, + 2 => Anchor::TopLeft, + _ => unreachable!(), + }, + } +} + +fn random_polygon() -> Object { + let number_of_anchors = rand::thread_rng().gen_range(2..7); + let start = random_anchor(); + let mut lines: Vec = vec![]; + let mut last_anchor = start.clone(); + for _ in 0..number_of_anchors { + let next_anchor = random_anchor(); + lines.push(random_line(next_anchor)); + last_anchor = next_anchor.clone(); + } + Object::Polygon(start, lines) +} + +fn random_line(end: Anchor) -> Line { + match rand::thread_rng().gen_range(1..=3) { + 1 => Line::Line(end), + 2 => Line::InwardCurve(end), + 3 => Line::OutwardCurve(end), + _ => unreachable!(), + } +} + +fn random_anchor() -> Anchor { + match rand::thread_rng().gen_range(1..=9) { + 1 => Anchor::TopLeft, + 2 => Anchor::Top, + 3 => Anchor::TopRight, + 4 => Anchor::Left, + 5 => Anchor::Center, + 6 => Anchor::Right, + 7 => Anchor::BottomLeft, + 8 => Anchor::Bottom, + 9 => Anchor::BottomRight, + _ => unreachable!(), + } +} + +fn random_center_anchor() -> CenterAnchor { + match rand::thread_rng().gen_range(1..=5) { + 1 => CenterAnchor::TopLeft, + 2 => CenterAnchor::TopRight, + 3 => CenterAnchor::Center, + 4 => CenterAnchor::BottomLeft, + 5 => CenterAnchor::BottomRight, + _ => unreachable!(), + } +} + +fn random_fill() -> Fill { + Fill::Solid(random_color()) + // match rand::thread_rng().gen_range(1..=3) { + // 1 => Fill::Solid(random_color()), + // 2 => Fill::Hatched, + // 3 => Fill::Dotted, + // _ => unreachable!(), + // } +} + +fn random_color() -> Color { + match rand::thread_rng().gen_range(1..=12) { + 1 => Color::Black, + 2 => Color::White, + 3 => Color::Red, + 4 => Color::Green, + 5 => Color::Blue, + 6 => Color::Yellow, + 7 => Color::Orange, + 8 => Color::Purple, + 9 => Color::Brown, + 10 => Color::Pink, + 11 => Color::Gray, + 12 => Color::Cyan, + _ => unreachable!(), + } } #[derive(Debug)] struct Shape { - name: String, + name: &'static str, objects: Vec<(Object, Option)>, } #[derive(Debug)] enum Object { - Polygon(Vec), + Polygon(Anchor, Vec), + Line(Anchor, Anchor), + CurveOutward(Anchor, Anchor), + CurveInward(Anchor, Anchor), SmallCircle(Anchor), Dot(Anchor), BigCircle(CenterAnchor), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Anchor { Top, TopRight, @@ -68,6 +238,23 @@ enum Anchor { Center, } +impl Anchor { + fn x(&self) -> f32 { + match self { + Anchor::TopLeft | Anchor::Left | Anchor::BottomLeft => 0.0, + Anchor::Top | Anchor::Center | Anchor::Bottom => 50.0, + Anchor::TopRight | Anchor::Right | Anchor::BottomRight => 100.0, + } + } + fn y(&self) -> f32 { + match self { + Anchor::TopLeft | Anchor::Top | Anchor::TopRight => 0.0, + Anchor::Left | Anchor::Center | Anchor::Right => 50.0, + Anchor::BottomLeft | Anchor::Bottom | Anchor::BottomRight => 100.0, + } + } +} + #[derive(Debug, Clone)] enum CenterAnchor { TopLeft, @@ -77,139 +264,411 @@ enum CenterAnchor { Center, } -#[derive(Debug, Clone)] +impl CenterAnchor { + fn x(&self) -> f32 { + match self { + CenterAnchor::TopLeft | CenterAnchor::BottomLeft => 25.0, + CenterAnchor::TopRight | CenterAnchor::BottomRight => 75.0, + CenterAnchor::Center => 50.0, + } + } + + fn y(&self) -> f32 { + match self { + CenterAnchor::TopLeft | CenterAnchor::TopRight => 25.0, + CenterAnchor::BottomLeft | CenterAnchor::BottomRight => 75.0, + CenterAnchor::Center => 50.0, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] enum Line { - Line(Anchor, Anchor), - InwardCurve(Anchor, Anchor), - OutwardCurve(Anchor, Anchor), + Line(Anchor), + InwardCurve(Anchor), + OutwardCurve(Anchor), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] enum Fill { Solid(Color), Hatched, Dotted, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] enum Color { - Named(ColorName), - RGBA(u8, u8, u8, u8), -} - -#[derive(Debug, Clone)] -enum ColorName { Black, White, - Grey, Red, Green, Blue, Yellow, - Cyan, - Magenta, Orange, + Purple, + Brown, + Cyan, + Pink, + Gray, } -fn parser() -> impl Parser, Error = Simple> { - let anchor = choice(( - just("top").to(Anchor::Top), - just("top right").to(Anchor::TopRight), - just("right").to(Anchor::Right), - just("bottom right").to(Anchor::BottomRight), - just("bottom").to(Anchor::Bottom), - just("bottom left").to(Anchor::BottomLeft), - just("left").to(Anchor::Left), - just("top left").to(Anchor::TopLeft), - just("center").to(Anchor::Center), - )); - let center_anchor = choice(( - just("top left").to(CenterAnchor::TopLeft), - just("top right").to(CenterAnchor::TopRight), - just("bottom left").to(CenterAnchor::BottomLeft), - just("bottom right").to(CenterAnchor::BottomRight), - just("center").to(CenterAnchor::Center), - )); - - let straight_line = anchor - .then_ignore(just("--").padded()) - .then(anchor) - .map(|(a, b)| Line::Line(a, b)); - let inward_curve = anchor - .then_ignore(one_of("n(").padded()) - .then(anchor) - .map(|(a, b)| Line::InwardCurve(a, b)); - let outward_curve = anchor - .then_ignore(one_of("u(").padded()) - .then(anchor) - .map(|(a, b)| Line::OutwardCurve(a, b)); - let line = choice((straight_line, inward_curve, outward_curve)); - let polygon = line.padded().repeated().boxed().map(Object::Polygon); - let point = anchor - .then_ignore(whitespace()) - .then_ignore(just("point")) - .map(Object::SmallCircle); - let circle = center_anchor - .then_ignore(whitespace()) - .then_ignore(just("circle")) - .map(Object::BigCircle); - let dot = anchor - .then_ignore(whitespace()) - .then_ignore(just("dot")) - .map(Object::Dot); - let object = choice((polygon, point, dot, circle)); - let color = choice(( - just("black").to(Color::Named(ColorName::Black)), - just("white").to(Color::Named(ColorName::White)), - just("grey").to(Color::Named(ColorName::Grey)), - just("red").to(Color::Named(ColorName::Red)), - just("green").to(Color::Named(ColorName::Green)), - just("blue").to(Color::Named(ColorName::Blue)), - just("yellow").to(Color::Named(ColorName::Yellow)), - just("cyan").to(Color::Named(ColorName::Cyan)), - just("magenta").to(Color::Named(ColorName::Magenta)), - just("orange").to(Color::Named(ColorName::Orange)), - just("#").ignored().then(text::int(16)).map(|(_, i)| { - Color::RGBA( - ((i >> 24) & 0xFF) as u8, - ((i >> 16) & 0xFF) as u8, - ((i >> 8) & 0xFF) as u8, - (i & 0xFF) as u8, - ) - }), - )); - let fill = choice(( - just("filled with") - .ignored() - .then_ignore(whitespace()) - .then(color) - .map(|(_, c)| Fill::Solid(c)), - just("hatched").to(Fill::Hatched), - just("dotted").to(Fill::Dotted), - )); - let filled_object = just("[") - .padded() - .ignored() - .then(object) - .then_ignore(just("]").padded()) - .then(fill) - .map(|((_, o), f)| (o, Some(f))); - let separator = newline().then_ignore(whitespace()).then_ignore(newline()); - let header = take_until(just(":")) - .then_ignore(whitespace()) - .then_ignore(newline()) - .map(|(i, _)| i); - let shape = header - .then_ignore(whitespace()) - .then( - filled_object - .or(object.map(|o| (o, None))) - .separated_by(newline()), - ) - .map(|(name, objects)| Shape { - name: name.into_iter().collect(), - objects, - }); +impl Color { + fn to_string(self) -> String { + match self { + Color::Black => "black", + Color::White => "white", + Color::Red => "red", + Color::Green => "green", + Color::Blue => "blue", + Color::Yellow => "yellow", + Color::Orange => "orange", + Color::Purple => "purple", + Color::Brown => "brown", + Color::Cyan => "cyan", + Color::Pink => "pink", + Color::Gray => "gray", + } + .to_string() + } +} + +impl Shape { + fn render(self) -> String { + let default_color = "black"; + let mut output = String::new(); + let mut svg = svg::Document::new(); + for (object, maybe_fill) in self.objects { + let mut group = svg::node::element::Group::new(); + match object { + Object::Polygon(start, lines) => { + println!("render: polygon({:?}, {:?})", start, lines); + let mut path = svg::node::element::path::Data::new(); + path = path.move_to((start.x(), start.y())); + for line in lines { + path = match line { + Line::Line(end) | Line::InwardCurve(end) | Line::OutwardCurve(end) => { + path.line_to((end.x(), end.y())) + } + }; + // path = match line { + // Line::Line(end) => path.line_to((end.x(), end.y())), + // Line::InwardCurve(end) => path.quadratic_curve_to(( + // (end.x(), end.y()), + // ((start.x() + end.x()) / 2.0, (start.y() + end.y()) / 2.0), + // )), + // Line::OutwardCurve(end) => path.quadratic_curve_to(( + // (end.x(), end.y()), + // ((start.x() + end.x()) / 2.0, (start.y() + end.y()) / 2.0), + // )), + // }; + } + path = path.close(); + group = group + .add(svg::node::element::Path::new().set("d", path)) + .set( + "style", + match maybe_fill { + // TODO + Some(Fill::Solid(color)) => { + format!("fill: {};", color.to_string()) + } + _ => format!("fill: none; stroke: {}; stroke-width: 0.5px;", default_color), + }, + ); + } + Object::Line(start, end) => { + println!("render: line({:?}, {:?})", start, end); + group = group.add( + svg::node::element::Line::new() + .set("x1", start.x()) + .set("y1", start.y()) + .set("x2", end.x()) + .set("y2", end.y()) + .set( + "style", + match maybe_fill { + // TODO + Some(Fill::Solid(color)) => { + format!("fill: none; stroke: {}; stroke-width: 2px;", color.to_string()) + } + _ => format!("fill: none; stroke: {}; stroke-width: 2px;", default_color), + }, + ), + ); + } + Object::CurveInward(start, end) | Object::CurveOutward(start, end) => { + let inward = if matches!(object, Object::CurveInward(_, _)) { + println!("render: curve_inward({:?}, {:?})", start, end); + true + } else { + println!("render: curve_outward({:?}, {:?})", start, end); + false + }; - shape.separated_by(separator) + let midpoint = ((start.x() + end.x()) / 2.0, (start.y() + end.y()) / 2.0); + let start_from_midpoint = (start.x() - midpoint.0, start.y() - midpoint.1); + let end_from_midpoint = (end.x() - midpoint.0, end.y() - midpoint.1); + println!(" midpoint: {:?}", midpoint); + println!( + " from midpoint: {:?} -> {:?}", + start_from_midpoint, end_from_midpoint + ); + let control = { + let relative = (end.x() - start.x(), end.y() - start.y()); + println!(" relative: {:?}", relative); + // diagonal line is going like this: \ + if start_from_midpoint.0 * start_from_midpoint.1 > 0.0 + && end_from_midpoint.0 * end_from_midpoint.1 > 0.0 + { + println!(" diagonal \\"); + if inward { + ( + midpoint.0 + relative.0.abs() / 2.0, + midpoint.1 - relative.1.abs() / 2.0, + ) + } else { + ( + midpoint.0 - relative.0.abs() / 2.0, + midpoint.1 + relative.1.abs() / 2.0, + ) + } + // diagonal line is going like this: / + } else if start_from_midpoint.0 * start_from_midpoint.1 < 0.0 + && end_from_midpoint.0 * end_from_midpoint.1 < 0.0 + { + println!(" diagonal /"); + if inward { + ( + midpoint.0 - relative.0.abs() / 2.0, + midpoint.1 - relative.1.abs() / 2.0, + ) + } else { + ( + midpoint.0 + relative.0.abs() / 2.0, + midpoint.1 + relative.1.abs() / 2.0, + ) + } + // line is horizontal + } else if start.y() == end.y() { + println!(" horizontal"); + ( + midpoint.0, + midpoint.1 + + (if inward { -1.0 } else { 1.0 }) * relative.0.abs() / 2.0, + ) + // line is vertical + } else if start.x() == end.x() { + println!(" vertical"); + ( + midpoint.0 + + (if inward { -1.0 } else { 1.0 }) * relative.1.abs() / 2.0, + midpoint.1, + ) + } else { + unreachable!() + } + }; + println!(" control: {:?}", control); + group = group.add( + svg::node::element::Path::new() + .set( + "d", + svg::node::element::path::Data::new() + .move_to((start.x(), start.y())) + .quadratic_curve_to((control, (end.x(), end.y()))), + ) + .set( + "style", + match maybe_fill { + // TODO + Some(Fill::Solid(color)) => { + format!("fill: none; stroke: {}; stroke-width: 2px;", color.to_string()) + } + _ => format!("fill: none; stroke: {}; stroke-width: 2px;", default_color), + }, + ), + ); + } + Object::SmallCircle(center) => { + println!("render: small_circle({:?})", center); + group = group.add( + svg::node::element::Circle::new() + .set("cx", center.x()) + .set("cy", center.y()) + .set("r", 5) + .set( + "style", + match maybe_fill { + // TODO + Some(Fill::Solid(color)) => { + format!("fill: {};", color.to_string()) + } + _ => format!("fill: none; stroke: {}; stroke-width: 0.5px;", default_color), + }, + ), + ); + } + Object::Dot(center) => { + println!("render: dot({:?})", center); + group = group.add( + svg::node::element::Circle::new() + .set("cx", center.x()) + .set("cy", center.y()) + .set("r", 2) + .set( + "style", + match maybe_fill { + // TODO + Some(Fill::Solid(color)) => { + format!("fill: {};", color.to_string()) + } + _ => format!("fill: none; stroke: {}; stroke-width: 0.5px;", default_color), + }, + ), + ); + } + Object::BigCircle(center) => { + println!("render: big_circle({:?})", center); + group = group.add( + svg::node::element::Circle::new() + .set("cx", center.x()) + .set("cy", center.y()) + .set("r", 25) + .set( + "style", + match maybe_fill { + // TODO + Some(Fill::Solid(color)) => { + format!("fill: {};", color.to_string()) + } + _ => format!("fill: none; stroke: {}; stroke-width: 0.5px;", default_color), + }, + ), + ); + } + } + println!(" fill: {:?}", &maybe_fill); + svg = svg.add(group); + } + // render a dotted grid + if false { + for x in vec![0, 25, 50, 75, 100] { + for y in vec![0, 25, 50, 75, 100] { + svg = svg.add( + svg::node::element::Circle::new() + .set("cx", x) + .set("cy", y) + .set("r", 0.5) + .set("fill", "gray"), + ); + } + } + } + svg.set("viewBox", "-10 -10 120 120").to_string() + } +} + +impl Object { + fn closed(self) -> bool { + matches!( + self, + Object::Polygon(_, _) | Object::BigCircle(_) | Object::SmallCircle(_) | Object::Dot(_) + ) + } } + +// fn parser() -> impl Parser, Error = Simple> { +// let anchor = choice(( +// just("top").to(Anchor::Top), +// just("top right").to(Anchor::TopRight), +// just("right").to(Anchor::Right), +// just("bottom right").to(Anchor::BottomRight), +// just("bottom").to(Anchor::Bottom), +// just("bottom left").to(Anchor::BottomLeft), +// just("left").to(Anchor::Left), +// just("top left").to(Anchor::TopLeft), +// just("center").to(Anchor::Center), +// )); +// let center_anchor = choice(( +// just("top left").to(CenterAnchor::TopLeft), +// just("top right").to(CenterAnchor::TopRight), +// just("bottom left").to(CenterAnchor::BottomLeft), +// just("bottom right").to(CenterAnchor::BottomRight), +// just("center").to(CenterAnchor::Center), +// )); + +// let straight_line = anchor.clone() +// .then_ignore(just("--").padded()) +// .then(anchor) +// .map(|(a, b)| Line::Line(a, b)); +// let inward_curve = &anchor +// .then_ignore(one_of("n(").padded()) +// .then(&anchor) +// .map(|(a, b)| Line::InwardCurve(a, b)); +// let outward_curve = &anchor +// .then_ignore(one_of("u(").padded()) +// .then(&anchor) +// .map(|(a, b)| Line::OutwardCurve(a, b)); +// let line = choice((straight_line, inward_curve, outward_curve)); +// let polygon = line.padded().repeated().boxed().map(Object::Polygon); +// let point = &anchor +// .then_ignore(whitespace()) +// .then_ignore(just("point")) +// .map(Object::SmallCircle); +// let circle = center_anchor +// .then_ignore(whitespace()) +// .then_ignore(just("circle")) +// .map(Object::BigCircle); +// let dot = &anchor +// .then_ignore(whitespace()) +// .then_ignore(just("dot")) +// .map(Object::Dot); +// let object = choice((polygon, point, dot, circle)); +// // let color = choice(( +// // text::ident().map(Color::Named), +// // just("#").ignored().then(text::int(16)).map(|(_, i)| { +// // Color::RGBA( +// // ((i >> 24) & 0xFF) as u8, +// // ((i >> 16) & 0xFF) as u8, +// // ((i >> 8) & 0xFF) as u8, +// // (i & 0xFF) as u8, +// // ) +// // }), +// // )); +// // let fill = choice(( +// // just("filled with") +// // .ignored() +// // .then_ignore(whitespace()) +// // .then(color) +// // .map(|(_, c)| Fill::Solid(c)), +// // just("hatched").to(Fill::Hatched), +// // just("dotted").to(Fill::Dotted), +// // )); +// // let filled_object = just("[") +// // .padded() +// // .ignored() +// // .then(object) +// // .then_ignore(just("]").padded()) +// // .then(fill) +// // .map(|((_, o), f)| (o, Some(f))); +// // let separator = newline().then_ignore(whitespace()).then_ignore(newline()); +// // let header = take_until(just(":")) +// // .then_ignore(whitespace()) +// // .then_ignore(newline()) +// // .map(|(i, _)| i); +// // let shape = header +// // .then_ignore(whitespace()) +// // .then( +// // filled_object +// // .or(object.map(|o| (o, None))) +// // .separated_by(newline()), +// // ) +// // .map(|(name, objects)| Shape { +// // name: name.into_iter().collect(), +// // objects, +// // }); +// // filled_object +// // .or(object.map(|o| (o, None))) +// // .separated_by(newline()) +// object.separated_by(newline()) +// }