From 3d86835849b3b30104ec0e2cdba2b9fbbebdc87e Mon Sep 17 00:00:00 2001 From: Ingolf Wagner Date: Thu, 8 Apr 2021 22:06:39 +0200 Subject: [PATCH 1/8] feat: add asteroids.rs --- pool/asteroids/ship1.svg | 34 +++++++++++++++ pool/asteroids/ship2.svg | 34 +++++++++++++++ pool/asteroids/ship3.svg | 34 +++++++++++++++ src/bin/asteroids.rs | 91 ++++++++++++++++++++++++++++++++++++++++ src/context.rs | 18 ++++++-- 5 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 pool/asteroids/ship1.svg create mode 100644 pool/asteroids/ship2.svg create mode 100644 pool/asteroids/ship3.svg create mode 100644 src/bin/asteroids.rs diff --git a/pool/asteroids/ship1.svg b/pool/asteroids/ship1.svg new file mode 100644 index 0000000..7837460 --- /dev/null +++ b/pool/asteroids/ship1.svg @@ -0,0 +1,34 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/pool/asteroids/ship2.svg b/pool/asteroids/ship2.svg new file mode 100644 index 0000000..580b04c --- /dev/null +++ b/pool/asteroids/ship2.svg @@ -0,0 +1,34 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/pool/asteroids/ship3.svg b/pool/asteroids/ship3.svg new file mode 100644 index 0000000..439e017 --- /dev/null +++ b/pool/asteroids/ship3.svg @@ -0,0 +1,34 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/bin/asteroids.rs b/src/bin/asteroids.rs new file mode 100644 index 0000000..e3ed65f --- /dev/null +++ b/src/bin/asteroids.rs @@ -0,0 +1,91 @@ +use geo::algorithm::intersects::Intersects; +use geo::algorithm::rotate::Rotate; +use geo::prelude::Translate; +use geo::rotate::RotatePoint; +use geo::{Coordinate, MultiPolygon, Point, Polygon}; +use geo::{LineString}; +use geo_clipper::Clipper; +use polygon_art::MultiPolygonExt; +use polygon_art::{load_multipolgon_from_svg, Context}; +use rand::prelude::StdRng; +use rand::Rng; + +fn main() { + let mut context = Context::create(); + let mut rng = context.gen_rng(); + + let ship_width = 100.; + let center_point = MultiPolygon(vec![create_asteroid(&mut rng, 20, 2)]) + .scale_to_width(2.5 * ship_width) + .translate(context.width / 2., context.height / 2.); + + // create asteroid + let mut asteroids: Vec> = Vec::new(); + for _ in 0..40 { + let without_orbit = rng.gen_range(40..140) as f64; + let with_orbit = without_orbit * 1.32; + let x = rng.gen_range(0..(context.width as i32)) as f64; + let y = rng.gen_range(0..(context.height as i32)) as f64; + let asteroid = MultiPolygon(vec![create_asteroid(&mut rng, 20, 15)]) + .scale_to_width(with_orbit) + .translate(x, y); + + if !center_point.intersects(&asteroid) && asteroids + .iter() + .find(|&m| *&m.intersects(&asteroid)) + .is_none() + { + asteroids.push(asteroid.scale_to_width(without_orbit)); + } + if asteroids.len() > 23 { + break; + } + } + + // load ships + let rotation = rng.gen_range(0..360); + let ship = load_multipolgon_from_svg("pool/asteroids/ship3.svg") + .unwrap() + .scale_to_width(ship_width) + .translate_center() + .rotate(rotation as f64); + context.translate_center(); + context.draw_multipolygon(&ship); + context.restore(); + + // drawing and clipping + let frame_space = 10.; + let window = context.frame(frame_space, frame_space, frame_space, frame_space); + context.draw_polygon(&window); + + for asteroid in asteroids.iter() { + context.draw_multipolygon(&asteroid.intersection(&window, 100.)); + } + + context.render(); +} + +fn create_asteroid(rng: &mut StdRng, subdivisions: i32, radius_randomization: i32) -> Polygon { + let mut asteroid_points: Vec> = Vec::new(); + for _ in 0..subdivisions { + let change = rng.gen_range(0..(radius_randomization * 2)); + asteroid_points.push(Point(Coordinate { + x: 0.0, + y: 100.0 + (change - radius_randomization) as f64, + })); + for point in asteroid_points.iter_mut() { + let (x, y) = point + .rotate_around_point( + 360. / subdivisions as f64, + Point(Coordinate { x: 0., y: 0. }), + ) + .x_y(); + point.set_x(x); + point.set_y(y); + } + } + Polygon::new( + LineString(asteroid_points.iter().map(|point| point.0).collect()), + Vec::new(), + ) +} diff --git a/src/context.rs b/src/context.rs index ab115d7..8983be5 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,6 @@ use cairo::Format; use cairo::{ImageSurface, SvgSurface}; -use geo_types::{LineString, MultiPolygon, Polygon}; +use geo::{polygon, LineString, MultiPolygon, Polygon}; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; @@ -22,6 +22,16 @@ pub struct Context { } impl Context { + /// generate a frame for the image + pub fn frame(&mut self, left: f64, bottom: f64, right: f64, top: f64) -> Polygon { + polygon![ + (x: left, y: top), + (x: self.width - right, y: top), + (x: self.width - right, y: self.height - bottom), + (x: left , y: self.height - bottom), + ] + } + pub fn draw_multipolygon(&self, multi_polygon: &MultiPolygon) { for polygon in multi_polygon.iter() { self.draw_polygon(polygon); @@ -48,10 +58,7 @@ impl Context { } context.stroke(); } -} -// cairo facades -impl Context { /// restore state which was saved before by the context pub fn restore(&self) { self.render_container.context().restore(); @@ -81,6 +88,9 @@ impl Context { self.render_container.render(); } + pub fn gen_rng(&mut self) -> StdRng { + StdRng::seed_from_u64(self.pseudo_random_number_generator.gen()) + } pub fn create() -> Context { let opt = Opt::get_command_line_arguments(); let height = f64::from(opt.height.clone()); From 4af31ce1854d3b818db58c3abaf433e210366af7 Mon Sep 17 00:00:00 2001 From: Ingolf Wagner Date: Fri, 9 Apr 2021 08:19:09 +0200 Subject: [PATCH 2/8] feat: add predefined formats --- src/bin/asteroids.rs | 44 +++++++++++++++++++++++++------------- src/context.rs | 26 +++++++++++++++++----- src/context/commandline.rs | 19 +++++++++++++++- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/bin/asteroids.rs b/src/bin/asteroids.rs index e3ed65f..076ff93 100644 --- a/src/bin/asteroids.rs +++ b/src/bin/asteroids.rs @@ -2,8 +2,8 @@ use geo::algorithm::intersects::Intersects; use geo::algorithm::rotate::Rotate; use geo::prelude::Translate; use geo::rotate::RotatePoint; +use geo::LineString; use geo::{Coordinate, MultiPolygon, Point, Polygon}; -use geo::{LineString}; use geo_clipper::Clipper; use polygon_art::MultiPolygonExt; use polygon_art::{load_multipolgon_from_svg, Context}; @@ -14,15 +14,28 @@ fn main() { let mut context = Context::create(); let mut rng = context.gen_rng(); - let ship_width = 100.; - let center_point = MultiPolygon(vec![create_asteroid(&mut rng, 20, 2)]) - .scale_to_width(2.5 * ship_width) - .translate(context.width / 2., context.height / 2.); + let width = context.width; + let height = context.height; + let diagonal = f64::sqrt(width * width + height * height); + + // todo : it does not make to much sense to define all parameters like this + // todo : it makes more sense to create a static (diagonal / width /height ratio ) setup and scale it up + let frame_offset = diagonal * 0.01; + let max_asteroid = (diagonal / 40.) as usize; + + let min_asteroid_size = diagonal / 40.; + let max_asteroid_size = diagonal / 12.; + let ship_length = diagonal / 23.; + let ship_orbit = 3.0; + + let center_point = MultiPolygon(vec![create_asteroid(&mut rng, 8, 2)]) + .scale_to_width(ship_orbit * ship_length) + .translate(width / 2., height / 2.); // create asteroid let mut asteroids: Vec> = Vec::new(); - for _ in 0..40 { - let without_orbit = rng.gen_range(40..140) as f64; + for _ in 0..(max_asteroid * 2) { + let without_orbit = rng.gen_range(min_asteroid_size..max_asteroid_size) as f64; let with_orbit = without_orbit * 1.32; let x = rng.gen_range(0..(context.width as i32)) as f64; let y = rng.gen_range(0..(context.height as i32)) as f64; @@ -30,14 +43,15 @@ fn main() { .scale_to_width(with_orbit) .translate(x, y); - if !center_point.intersects(&asteroid) && asteroids - .iter() - .find(|&m| *&m.intersects(&asteroid)) - .is_none() + if (!center_point.intersects(&asteroid)) + && asteroids + .iter() + .find(|&m| *&m.intersects(&asteroid)) + .is_none() { asteroids.push(asteroid.scale_to_width(without_orbit)); } - if asteroids.len() > 23 { + if asteroids.len() > max_asteroid { break; } } @@ -46,7 +60,8 @@ fn main() { let rotation = rng.gen_range(0..360); let ship = load_multipolgon_from_svg("pool/asteroids/ship3.svg") .unwrap() - .scale_to_width(ship_width) + .rotate(90.) // rotate to scale ship with width + .scale_to_width(ship_length) .translate_center() .rotate(rotation as f64); context.translate_center(); @@ -54,8 +69,7 @@ fn main() { context.restore(); // drawing and clipping - let frame_space = 10.; - let window = context.frame(frame_space, frame_space, frame_space, frame_space); + let window = context.frame(frame_offset, frame_offset, frame_offset, frame_offset); context.draw_polygon(&window); for asteroid in asteroids.iter() { diff --git a/src/context.rs b/src/context.rs index 8983be5..0919b70 100644 --- a/src/context.rs +++ b/src/context.rs @@ -91,12 +91,29 @@ impl Context { pub fn gen_rng(&mut self) -> StdRng { StdRng::seed_from_u64(self.pseudo_random_number_generator.gen()) } + pub fn create() -> Context { let opt = Opt::get_command_line_arguments(); - let height = f64::from(opt.height.clone()); - let width = f64::from(opt.width.clone()); let line_size = opt.line_size.clone(); + let mm = 3.779527547619048; // calculated with inkscape 1587.40157 (px) / 420 (mm) (from a3) + let (mut height, mut width) = match opt.format { + None => (f64::from(opt.height.clone()), f64::from(opt.width.clone())), + Some(format) => match &format[..] { + "a3" => (297. * mm, 420. * mm), + "a4" => (210. * mm, 297. * mm), + "a5" => (148. * mm, 210. * mm), + "x220" => (768., 1366.), + _ => (f64::from(opt.height.clone()), f64::from(opt.width.clone())), + }, + }; + + if opt.orientation == "portrait" { + let cache = height; + height = width; + width = cache; + } + let random_seed = match opt.random_seed { Some(random_seed) => random_seed, None => match opt.random_seed_date { @@ -128,9 +145,8 @@ impl Context { .expect("Can't svg surface"); Box::new(SvgRenderContainer::new(svg_surface, palette)) as Box } else if opt.output_type == "png" { - let png_surface = - ImageSurface::create(Format::Rgb24, opt.width.clone(), opt.height.clone()) - .expect("Can't create png surface"); + let png_surface = ImageSurface::create(Format::Rgb24, width as i32, height as i32) + .expect("Can't create png surface"); Box::new(PngRenderContainer::new( opt.output.to_string_lossy().to_string(), png_surface, diff --git a/src/context/commandline.rs b/src/context/commandline.rs index f019227..b246a6c 100644 --- a/src/context/commandline.rs +++ b/src/context/commandline.rs @@ -13,13 +13,30 @@ pub struct Opt { pub output: PathBuf, /// define width in pixels - #[structopt(long, default_value = "400")] + #[structopt(long, default_value = "600")] pub width: i32, /// define height in pixels #[structopt(long, default_value = "400")] pub height: i32, + /// use a predefined format + #[structopt( + long, + conflicts_with="height", + conflicts_with="width", + possible_values=&["a3", "a4", "a5", "x220"] + )] + pub format: Option, + + /// change orientation + #[structopt( + long, + default_value="landscape", + possible_values=&["portrait", "landscape"] + )] + pub orientation: String, + /// define line size in pixels, all lines will be this thick #[structopt(long, default_value = "1.0")] pub line_size: f64, From 65ff198dba4d6806baaa43db8be623cec2195517 Mon Sep 17 00:00:00 2001 From: Ingolf Wagner Date: Sat, 10 Apr 2021 03:25:03 +0200 Subject: [PATCH 3/8] feat: show how to inject svg into binary --- examples/svg.rs | 4 ++-- src/bin/asteroids.rs | 11 +++++++++-- src/lib.rs | 3 ++- src/svg.rs | 22 +++++++++++++++++++--- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/examples/svg.rs b/examples/svg.rs index 456f5f1..f45ee6f 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -3,7 +3,7 @@ use geo::rotate::RotatePoint; use geo::translate::Translate; use geo::{Coordinate, Point}; -use polygon_art::load_multipolgon_from_svg; +use polygon_art::load_multipolygon_from_svg; use polygon_art::Context; use polygon_art::MultiPolygonExt; use rand::Rng; @@ -12,7 +12,7 @@ fn main() { let mut renderer = Context::create(); let path = "pool/examples/example.svg"; - let svg = load_multipolgon_from_svg(path).expect("couldn't load svg"); + let svg = load_multipolygon_from_svg(path).expect("couldn't load svg"); let item_space = 110.; diff --git a/src/bin/asteroids.rs b/src/bin/asteroids.rs index 076ff93..778bada 100644 --- a/src/bin/asteroids.rs +++ b/src/bin/asteroids.rs @@ -6,10 +6,11 @@ use geo::LineString; use geo::{Coordinate, MultiPolygon, Point, Polygon}; use geo_clipper::Clipper; use polygon_art::MultiPolygonExt; -use polygon_art::{load_multipolgon_from_svg, Context}; +use polygon_art::{load_multipolygon_from_svg_include, Context}; use rand::prelude::StdRng; use rand::Rng; +/// A more or less complete example that illustrates various technics fn main() { let mut context = Context::create(); let mut rng = context.gen_rng(); @@ -58,7 +59,13 @@ fn main() { // load ships let rotation = rng.gen_range(0..360); - let ship = load_multipolgon_from_svg("pool/asteroids/ship3.svg") + let ship_yard = [ + include_str!("../../pool/asteroids/ship1.svg"), + include_str!("../../pool/asteroids/ship2.svg"), + include_str!("../../pool/asteroids/ship3.svg"), + ]; + let chosen_ship = rng.gen_range(0..3); + let ship = load_multipolygon_from_svg_include(ship_yard[chosen_ship]) .unwrap() .rotate(90.) // rotate to scale ship with width .scale_to_width(ship_length) diff --git a/src/lib.rs b/src/lib.rs index a284a6d..045802e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,4 +5,5 @@ mod multipolygon_ext; pub use crate::multipolygon_ext::MultiPolygonExt; mod svg; -pub use crate::svg::load_multipolgon_from_svg; +pub use crate::svg::load_multipolygon_from_svg; +pub use crate::svg::load_multipolygon_from_svg_include; diff --git a/src/svg.rs b/src/svg.rs index 2db2e28..287bfae 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -3,12 +3,28 @@ use geo_types::{Coordinate, LineString, MultiPolygon, Polygon}; use std::error::Error; use svg::node::element::tag::Path; use svg::parser::Event; +use svg::Parser; + +/// load a multipolygon dynamically at runtime +pub fn load_multipolygon_from_svg(path: &str) -> Result, Box> { + let mut content = String::new(); + let parser = svg::open(path, &mut content)?; + parse_svg(parser) +} + +/// load a multipolygon statically at compiletime +/// with include_str! +pub fn load_multipolygon_from_svg_include( + content: &str, +) -> Result, Box> { + let parser = svg::read(content)?; + parse_svg(parser) +} #[allow(non_upper_case_globals)] -pub fn load_multipolgon_from_svg(path: &str) -> Result, Box> { - let mut content = String::new(); +fn parse_svg(parser: Parser) -> Result, Box> { let mut result: Vec> = Vec::new(); - for event in svg::open(path, &mut content)? { + for event in parser { match event { Event::Tag(Path, _, attributes) => { let data = attributes.get("d").unwrap(); From 54fa1b222d9efc6de48bfbc59ec69a12ab358bcc Mon Sep 17 00:00:00 2001 From: Ingolf Wagner Date: Sat, 10 Apr 2021 13:33:56 +0200 Subject: [PATCH 4/8] feat: introduce simple font from asteroid game --- Cargo.lock | 192 +++++++++++++++++++++++++++++++++++++------ Cargo.toml | 2 +- examples/clipping.rs | 2 +- examples/font.rs | 47 +++++++++++ examples/svg.rs | 1 - shell.nix | 4 +- src/bin/asteroids.rs | 1 + src/context.rs | 17 ++-- src/font.rs | 133 ++++++++++++++++++++++++++++++ src/lib.rs | 4 + 10 files changed, 371 insertions(+), 32 deletions(-) create mode 100644 examples/font.rs create mode 100644 src/font.rs diff --git a/Cargo.lock b/Cargo.lock index 6ecd8ad..c5ec21a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,11 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "approx" version = "0.3.2" @@ -119,25 +124,26 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "cairo-sys-rs" -version = "0.9.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "system-deps 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -454,7 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "glib" -version = "0.9.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -463,19 +469,35 @@ dependencies = [ "futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-macros 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "glib-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "anyhow 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-crate 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.68 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "glib-sys" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "system-deps 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -485,12 +507,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "gobject-sys" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "system-deps 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -552,6 +574,14 @@ dependencies = [ "either 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "either 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -629,6 +659,11 @@ name = "object" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "palette" version = "0.5.0" @@ -709,7 +744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "polygon-art" version = "1.0.0" dependencies = [ - "cairo-rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-rs 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "geo 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "geo-clipper 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "geo-svg-io 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -726,6 +761,14 @@ name = "ppv-lite86" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "toml 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro-error" version = "0.4.9" @@ -738,6 +781,18 @@ dependencies = [ "syn 1.0.68 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-error-attr 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.68 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro-error-attr" version = "0.4.9" @@ -750,6 +805,16 @@ dependencies = [ "syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro-hack" version = "0.5.11" @@ -939,6 +1004,11 @@ dependencies = [ "syn 1.0.68 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "shlex" version = "0.1.1" @@ -996,6 +1066,22 @@ dependencies = [ "syn 1.0.68 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.68 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "svg" version = "0.9.1" @@ -1041,6 +1127,20 @@ dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "system-deps" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", + "strum 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", + "strum_macros 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)", + "version-compare 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -1057,6 +1157,24 @@ dependencies = [ "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thiserror-impl 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.68 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -1065,6 +1183,14 @@ dependencies = [ "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "typenum" version = "1.13.0" @@ -1090,6 +1216,11 @@ name = "vec_map" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "version-compare" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "version_check" version = "0.9.3" @@ -1150,6 +1281,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum adler 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" "checksum aho-corasick 0.7.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum anyhow 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" "checksum approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" "checksum as-slice 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" @@ -1159,8 +1291,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum byteorder 1.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" -"checksum cairo-rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "157049ba9618aa3a61c39d5d785102c04d3b1f40632a706c621a9aedc21e6084" -"checksum cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff65ba02cac715be836f63429ab00a767d48336efc5497c5637afb53b4f14d63" +"checksum cairo-rs 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c5c0f2e047e8ca53d0ff249c54ae047931d7a6ebe05d00af73e0ffeb6e34bdb8" +"checksum cairo-sys-rs 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2ed2639b9ad5f1d6efa76de95558e11339e7318426d84ac4890b86c03e828ca7" "checksum cc 1.0.67 (registry+https://github.com/rust-lang/crates.io-index)" = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" "checksum cexpr 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" @@ -1196,10 +1328,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" "checksum getrandom 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" "checksum gimli 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" -"checksum glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "40fb573a09841b6386ddf15fd4bc6655b4f5b106ca962f57ecaecde32a0061c0" -"checksum glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "95856f3802f446c05feffa5e24859fe6a183a7cb849c8449afc35c86b1e316e2" +"checksum glib 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +"checksum glib-macros 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +"checksum glib-sys 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" "checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" -"checksum gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31d1a804f62034eccf370006ccaef3708a71c31d561fee88564abe71177553d9" +"checksum gobject-sys 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" "checksum hash32 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" "checksum heapless 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" @@ -1207,6 +1340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum iso8601 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cee08a007a59a8adfc96f69738ddf59e374888dfd84b49c4b916543067644d58" "checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +"checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" @@ -1218,6 +1352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" "checksum num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" "checksum object 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" +"checksum once_cell 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" "checksum palette 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a05c0334468e62a4dfbda34b29110aa7d70d58c7fdb2c9857b5874dd9827cc59" "checksum palette_derive 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0b4b5f600e60dd3a147fb57b4547033d382d1979eb087af310e91cb45a63b1f4" "checksum pdqselect 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" @@ -1229,8 +1364,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" "checksum ppv-lite86 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +"checksum proc-macro-crate 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" "checksum proc-macro-error 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "052b3c9af39c7e5e94245f820530487d19eb285faedcb40e0c3275132293f242" +"checksum proc-macro-error 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" "checksum proc-macro-error-attr 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d175bef481c7902e63e3165627123fff3502f06ac043d3ef42d08c1246da9253" +"checksum proc-macro-error-attr 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" "checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" "checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" "checksum proc-macro2 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" @@ -1255,6 +1393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rustc-demangle 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" "checksum rustc-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" "checksum rustversion 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6" +"checksum serde 1.0.125 (registry+https://github.com/rust-lang/crates.io-index)" = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" "checksum shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" "checksum siphasher 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" @@ -1264,19 +1403,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum structopt 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "a1bcbed7d48956fcbb5d80c6b95aedb553513de0a1b451ea92679d999c010e98" "checksum structopt-derive 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "095064aa1f5b94d14e635d0a5684cf140c43ae40a0fd990708d38f5d669e5f64" +"checksum strum 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" +"checksum strum_macros 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" "checksum svg 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a95656b5b37251de210aa2751d538b6f31ce375bacf725a32223bfa6344e0776" "checksum svgtypes 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" "checksum syn 1.0.68 (registry+https://github.com/rust-lang/crates.io-index)" = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" "checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" "checksum synstructure 0.12.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +"checksum system-deps 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" "checksum termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum thiserror 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +"checksum thiserror-impl 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" "checksum threadpool 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +"checksum toml 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)" = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" "checksum typenum 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum version-compare 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" "checksum version_check 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" "checksum wasi 0.10.2+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" diff --git a/Cargo.toml b/Cargo.toml index acb5b65..5994e77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ maintenance = { status = "actively-developed" } exclude = [ "shell.nix", "default.nix" ] [dependencies] -cairo-rs = { version = "0.8", features = [ "png", "svg"] } +cairo-rs = { version = "0.9", features = [ "png", "svg" ] } geo = "0.16" geo-clipper = "0.4.0" geo-svg-io = "0.1.1" diff --git a/examples/clipping.rs b/examples/clipping.rs index 8d02833..03e4666 100644 --- a/examples/clipping.rs +++ b/examples/clipping.rs @@ -37,6 +37,7 @@ fn main() { context.draw_multipolygon(union); // intersection + context.save(); context.translate(0.0, 250.); let intersection = &polygon_a.intersection(&polygon_b, 10.0); context.draw_multipolygon(intersection); @@ -51,7 +52,6 @@ fn main() { context.translate(0., 250.); let difference = &polygon_b.difference(&polygon_a, 10.0); context.draw_multipolygon(difference); - context.restore(); context.render(); } diff --git a/examples/font.rs b/examples/font.rs new file mode 100644 index 0000000..0ddfad1 --- /dev/null +++ b/examples/font.rs @@ -0,0 +1,47 @@ +//! Shows how to use buildin font + +use geo::line_string; +use polygon_art::{AsteroidFont, Context, FontDirection}; +use FontDirection::{LeftRight, TopDown}; + +fn main() { + let context = Context::create(); + context.save(); + + context.translate(70., 50.); + // context.scale(8.); + + let font = AsteroidFont::new(); + let line = font.get_text("Hey there!".to_string(), 12., LeftRight); + for char in line.iter() { + context.draw_line_string(char); + } + context.draw_line_string(&line_string![(x : 0., y:14.), (x: 1000. , y : 14.)]); + + context.translate(0.0, 18.); + let line = font.get_text("How are you?".to_string(), 22., LeftRight); + for char in line.iter() { + context.draw_line_string(char); + } + + context.translate(0.0, 28.); + let line = font.get_text("Here a bitter text!".to_string(), 50., LeftRight); + for char in line.iter() { + context.draw_line_string(char); + } + + context.translate(0.0, 56.); + let line = font.get_text("0123456789".to_string(), 50., LeftRight); + for char in line.iter() { + context.draw_line_string(char); + } + + context.restore(); + context.translate(30., 50.); + let line = font.get_text("0123456789".to_string(), 30., TopDown); + for char in line.iter() { + context.draw_line_string(char); + } + + context.render(); +} diff --git a/examples/svg.rs b/examples/svg.rs index f45ee6f..5543e4d 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -43,6 +43,5 @@ fn main() { renderer.draw_multipolygon(&svg); } - renderer.restore(); renderer.render(); } diff --git a/shell.nix b/shell.nix index bd398cb..c01c489 100644 --- a/shell.nix +++ b/shell.nix @@ -12,6 +12,7 @@ pkgs.mkShell { clang llvm + pkg-config (pkgs.writers.writeBashBin "reformat" '' for file in `find ${toString ./.} -type f | egrep "\.rs$"` @@ -24,7 +25,8 @@ pkgs.mkShell { # run this on start # ----------------- shellHook = '' - export NIX_ENFORCE_PURITY=0 + # export NIX_ENFORCE_PURITY=0 + # Needed so bindgen can find libclang.so export LIBCLANG_PATH="${pkgs.llvmPackages.libclang}/lib"; ''; diff --git a/src/bin/asteroids.rs b/src/bin/asteroids.rs index 778bada..fc023f2 100644 --- a/src/bin/asteroids.rs +++ b/src/bin/asteroids.rs @@ -71,6 +71,7 @@ fn main() { .scale_to_width(ship_length) .translate_center() .rotate(rotation as f64); + context.save(); context.translate_center(); context.draw_multipolygon(&ship); context.restore(); diff --git a/src/context.rs b/src/context.rs index 0919b70..195dc0e 100644 --- a/src/context.rs +++ b/src/context.rs @@ -32,6 +32,11 @@ impl Context { ] } + /// a fall back operation, if you need a function not provided by this struct. + pub fn cairo_context(&self) -> &cairo::Context { + self.render_container.context() + } + pub fn draw_multipolygon(&self, multi_polygon: &MultiPolygon) { for polygon in multi_polygon.iter() { self.draw_polygon(polygon); @@ -39,13 +44,13 @@ impl Context { } pub fn draw_polygon(&self, polygon: &Polygon) { - self.plot_line_string(polygon.exterior()); + self.draw_line_string(polygon.exterior()); for interior in polygon.interiors() { - self.plot_line_string(interior); + self.draw_line_string(interior); } } - fn plot_line_string(&self, line_string: &LineString) { + pub fn draw_line_string(&self, line_string: &LineString) { let context = self.render_container.context(); let mut init = true; for point in line_string.clone().into_points() { @@ -64,10 +69,13 @@ impl Context { self.render_container.context().restore(); } + /// save state to restore later again + pub fn save(&self) { + self.render_container.context().save(); + } /// scale, but line with stays pub fn scale(&self, factor: f64) { let context = self.render_container.context(); - context.save(); context.scale(factor, factor); context.set_line_width(1. / factor); } @@ -80,7 +88,6 @@ impl Context { /// translate to a position pub fn translate(&self, tx: f64, ty: f64) { let context = self.render_container.context(); - context.save(); context.translate(tx, ty); } diff --git a/src/font.rs b/src/font.rs new file mode 100644 index 0000000..2b61642 --- /dev/null +++ b/src/font.rs @@ -0,0 +1,133 @@ +// props to https://trmm.net/Asteroids_font +// https://github.com/osresearch/vst/blob/master/teensyv/asteroids_font.c + +use geo::algorithm::translate::Translate; +use geo::{Coordinate, LineString}; +use std::collections::HashMap; + +pub struct AsteroidFont { + letters: HashMap>>, +} + +impl AsteroidFont { + #[rustfmt::skip] + pub fn new() -> Self { + let mut letters = HashMap::new(); + letters.insert('0', vec![vec![(0, 0), (8, 0), (8, 12), (0, 12), (0, 0), (8, 12)]]); + letters.insert('1', vec![vec![(4, 0), (4, 12), (3, 10)]]); + letters.insert('2', vec![vec![(0, 12), (8, 12), (8, 7), (0, 5), (0, 0), (8, 0)]]); + letters.insert('3', vec![vec![(0, 12), (8, 12), (8, 0), (0, 0), ], vec![(0, 6), (8, 6)]]); + letters.insert('4', vec![vec![(0, 12), (0, 6), (8, 6), ], vec![(8, 12), (8, 0)]]); + letters.insert('5', vec![vec![(0, 0), (8, 0), (8, 6), (0, 7), (0, 12), (8, 12)]]); + letters.insert('6', vec![vec![(0, 12), (0, 0), (8, 0), (8, 5), (0, 7)]]); + letters.insert('7', vec![vec![(0, 12), (8, 12), (8, 6), (4, 0)]]); + letters.insert('8', vec![vec![(0, 0), (8, 0), (8, 12), (0, 12), (0, 0), ], vec![(0, 6), (8, 6)]]); + letters.insert('9', vec![vec![(8, 0), (8, 12), (0, 12), (0, 7), (8, 5)]]); + letters.insert(' ', vec![]); + letters.insert('.', vec![vec![(3, 0), (4, 0)]]); + letters.insert(',', vec![vec![(2, 0), (4, 2)]]); + letters.insert('-', vec![vec![(2, 6), (6, 6)]]); + letters.insert('+', vec![vec![(1, 6), (7, 6), ], vec![(4, 9), (4, 3)]]); + letters.insert('!', vec![vec![(4, 0), (3, 2), (5, 2), (4, 0), ], vec![(4, 4), (4, 12)]]); + letters.insert('#', vec![vec![(0, 4), (8, 4), (6, 2), (6, 10), (8, 8), (0, 8), (2, 10), (2, 2)]]); + letters.insert('^', vec![vec![(2, 6), (4, 12), (6, 6)]]); + letters.insert('=', vec![vec![(1, 4), (7, 4), ], vec![(1, 8), (7, 8)]]); + letters.insert('*', vec![vec![(0, 0), (4, 12), (8, 0), (0, 8), (8, 8), (0, 0)]]); + letters.insert('_', vec![vec![(0, 0), (8, 0)]]); + letters.insert('/', vec![vec![(0, 0), (8, 12)]]); + letters.insert('\\', vec![vec![(0, 12), (8, 0)]]); + letters.insert('@', vec![vec![(8, 4), (4, 0), (0, 4), (0, 8), (4, 12), (8, 8), (4, 4), (3, 6)]]); + letters.insert('$', vec![vec![(6, 2), (2, 6), (6, 10), ], vec![(4, 12), (4, 0)]]); + letters.insert('&', vec![vec![(8, 0), (4, 12), (8, 8), (0, 4), (4, 0), (8, 4)]]); + letters.insert('[', vec![vec![(6, 0), (2, 0), (2, 12), (6, 12)]]); + letters.insert(']', vec![vec![(2, 0), (6, 0), (6, 12), (2, 12)]]); + letters.insert('(', vec![vec![(6, 0), (2, 4), (2, 8), (6, 12)]]); + letters.insert(')', vec![vec![(2, 0), (6, 4), (6, 8), (2, 12)]]); + letters.insert('{', vec![vec![(6, 0), (4, 2), (4, 10), (6, 12), ], vec![(2, 6), (4, 6)]]); + letters.insert('}', vec![vec![(4, 0), (6, 2), (6, 10), (4, 12), ], vec![(6, 6), (8, 6)]]); + letters.insert('%', vec![vec![(0, 0), (8, 12), ], vec![(2, 10), (2, 8), ], vec![(6, 4), (6, 2)]]); + letters.insert('<', vec![vec![(6, 0), (2, 6), (6, 12)]]); + letters.insert('>', vec![vec![(2, 0), (6, 6), (2, 12)]]); + letters.insert('|', vec![vec![(4, 0), (4, 5), ], vec![(4, 6), (4, 12)]]); + letters.insert(':', vec![vec![(4, 9), (4, 7), ], vec![(4, 5), (4, 3)]]); + letters.insert(';', vec![vec![(4, 9), (4, 7), ], vec![(4, 5), (1, 2)]]); + letters.insert('"', vec![vec![(2, 10), (2, 6), ], vec![(6, 10), (6, 6)]]); + letters.insert('\'', vec![vec![(2, 6), (6, 10)]]); + letters.insert('`', vec![vec![(2, 10), (6, 6)]]); + letters.insert('~', vec![vec![(0, 4), (2, 8), (6, 4), (8, 8)]]); + letters.insert('?', vec![vec![(0, 8), (4, 12), (8, 8), (4, 4), ], vec![(4, 1), (4, 0)]]); + letters.insert('a', vec![vec![(0, 0), (0, 8), (4, 12), (8, 8), (8, 0), ], vec![(0, 4), (8, 4)]]); + letters.insert('b', vec![vec![(0, 0), (0, 12), (4, 12), (8, 10), (4, 6), (8, 2), (4, 0), (0, 0)]]); + letters.insert('c', vec![vec![(8, 0), (0, 0), (0, 12), (8, 12)]]); + letters.insert('d', vec![vec![(0, 0), (0, 12), (4, 12), (8, 8), (8, 4), (4, 0), (0, 0)]]); + letters.insert('e', vec![vec![(8, 0), (0, 0), (0, 12), (8, 12), ], vec![(0, 6), (6, 6)]]); + letters.insert('f', vec![vec![(0, 0), (0, 12), (8, 12), ], vec![(0, 6), (6, 6)]]); + letters.insert('g', vec![vec![(6, 6), (8, 4), (8, 0), (0, 0), (0, 12), (8, 12)]]); + letters.insert('h', vec![vec![(0, 0), (0, 12), ], vec![(0, 6), (8, 6), ], vec![(8, 12), (8, 0)]]); + letters.insert('i', vec![vec![(0, 0), (8, 0), ], vec![(4, 0), (4, 12), ], vec![(0, 12), (8, 12)]]); + letters.insert('j', vec![vec![(0, 4), (4, 0), (8, 0), (8, 12)]]); + letters.insert('k', vec![vec![(0, 0), (0, 12), ], vec![(8, 12), (0, 6), (6, 0)]]); + letters.insert('l', vec![vec![(8, 0), (0, 0), (0, 12)]]); + letters.insert('m', vec![vec![(0, 0), (0, 12), (4, 8), (8, 12), (8, 0)]]); + letters.insert('n', vec![vec![(0, 0), (0, 12), (8, 0), (8, 12)]]); + letters.insert('o', vec![vec![(0, 0), (0, 12), (8, 12), (8, 0), (0, 0)]]); + letters.insert('p', vec![vec![(0, 0), (0, 12), (8, 12), (8, 6), (0, 5)]]); + letters.insert('q', vec![vec![(0, 0), (0, 12), (8, 12), (8, 4), (0, 0), ], vec![(4, 4), (8, 0)]]); + letters.insert('r', vec![vec![(0, 0), (0, 12), (8, 12), (8, 6), (0, 5), ], vec![(4, 5), (8, 0)]]); + letters.insert('s', vec![vec![(0, 2), (2, 0), (8, 0), (8, 5), (0, 7), (0, 12), (6, 12), (8, 10)]]); + letters.insert('t', vec![vec![(0, 12), (8, 12), ], vec![(4, 12), (4, 0)]]); + letters.insert('u', vec![vec![(0, 12), (0, 2), (4, 0), (8, 2), (8, 12)]]); + letters.insert('v', vec![vec![(0, 12), (4, 0), (8, 12)]]); + letters.insert('w', vec![vec![(0, 12), (2, 0), (4, 4), (6, 0), (8, 12)]]); + letters.insert('x', vec![vec![(0, 0), (8, 12), ], vec![(0, 12), (8, 0)]]); + letters.insert('y', vec![vec![(0, 12), (4, 6), (8, 12), ], vec![(4, 6), (4, 0)]]); + letters.insert('z', vec![vec![(0, 12), (8, 12), (0, 0), (8, 0), ], vec![(2, 6), (6, 6)]]); + AsteroidFont {letters } + } + + pub fn get_letter(&self, letter: &char, size: f64) -> Vec> { + let letter_coordinates = self.letters.get(&letter.to_ascii_lowercase()).unwrap(); + let mut polygons: Vec> = Vec::new(); + for letter_part in letter_coordinates.iter() { + let coordinates: Vec> = letter_part + .iter() + .map(|&(x, y)| Coordinate { + x: x as f64 * size / 12., + y: (12. - y as f64) * size / 12., + }) + .collect(); + polygons.push(LineString(coordinates)); + } + polygons + } + + pub fn get_text( + &self, + text: String, + size: f64, + direction: FontDirection, + ) -> Vec> { + let mut letters: Vec> = Vec::new(); + let mut count = 0.; + for char in text.chars() { + let letter = self.get_letter(&char, size); + for character in letter.iter() { + match direction { + FontDirection::LeftRight => { + letters.push(character.translate(count * 10.0 * size / 12., 0.)) + } + FontDirection::TopDown => { + letters.push(character.translate(0., count * 14.0 * size / 12.)) + } + } + } + count = count + 1.; + } + letters + } +} + +pub enum FontDirection { + LeftRight, + TopDown, +} diff --git a/src/lib.rs b/src/lib.rs index 045802e..c1aecd4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,7 @@ pub use crate::multipolygon_ext::MultiPolygonExt; mod svg; pub use crate::svg::load_multipolygon_from_svg; pub use crate::svg::load_multipolygon_from_svg_include; + +mod font; +pub use crate::font::AsteroidFont; +pub use crate::font::FontDirection; From e1694569582ca316804c9b214ded489d269a12b4 Mon Sep 17 00:00:00 2001 From: Ingolf Wagner Date: Mon, 12 Apr 2021 20:59:15 +0200 Subject: [PATCH 5/8] feat: add score board and life board --- examples/font.rs | 22 ++++-------------- src/bin/asteroids.rs | 55 +++++++++++++++++++++++++++++++++++++++----- src/context.rs | 8 +++++++ src/font.rs | 11 +++++++-- 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/examples/font.rs b/examples/font.rs index 0ddfad1..22b4cc5 100644 --- a/examples/font.rs +++ b/examples/font.rs @@ -7,41 +7,29 @@ use FontDirection::{LeftRight, TopDown}; fn main() { let context = Context::create(); context.save(); - context.translate(70., 50.); - // context.scale(8.); let font = AsteroidFont::new(); let line = font.get_text("Hey there!".to_string(), 12., LeftRight); - for char in line.iter() { - context.draw_line_string(char); - } + context.draw_line_strings(&line); context.draw_line_string(&line_string![(x : 0., y:14.), (x: 1000. , y : 14.)]); context.translate(0.0, 18.); let line = font.get_text("How are you?".to_string(), 22., LeftRight); - for char in line.iter() { - context.draw_line_string(char); - } + context.draw_line_strings(&line); context.translate(0.0, 28.); let line = font.get_text("Here a bitter text!".to_string(), 50., LeftRight); - for char in line.iter() { - context.draw_line_string(char); - } + context.draw_line_strings(&line); context.translate(0.0, 56.); let line = font.get_text("0123456789".to_string(), 50., LeftRight); - for char in line.iter() { - context.draw_line_string(char); - } + context.draw_line_strings(&line); context.restore(); context.translate(30., 50.); let line = font.get_text("0123456789".to_string(), 30., TopDown); - for char in line.iter() { - context.draw_line_string(char); - } + context.draw_line_strings(&line); context.render(); } diff --git a/src/bin/asteroids.rs b/src/bin/asteroids.rs index fc023f2..9f01cc2 100644 --- a/src/bin/asteroids.rs +++ b/src/bin/asteroids.rs @@ -3,10 +3,10 @@ use geo::algorithm::rotate::Rotate; use geo::prelude::Translate; use geo::rotate::RotatePoint; use geo::LineString; -use geo::{Coordinate, MultiPolygon, Point, Polygon}; +use geo::{polygon, Coordinate, MultiPolygon, Point, Polygon}; use geo_clipper::Clipper; -use polygon_art::MultiPolygonExt; use polygon_art::{load_multipolygon_from_svg_include, Context}; +use polygon_art::{AsteroidFont, FontDirection, MultiPolygonExt}; use rand::prelude::StdRng; use rand::Rng; @@ -65,9 +65,11 @@ fn main() { include_str!("../../pool/asteroids/ship3.svg"), ]; let chosen_ship = rng.gen_range(0..3); - let ship = load_multipolygon_from_svg_include(ship_yard[chosen_ship]) + let raw_ship = load_multipolygon_from_svg_include(ship_yard[chosen_ship]) .unwrap() - .rotate(90.) // rotate to scale ship with width + // rotate to scale ship with width + .rotate(90.); + let ship = raw_ship .scale_to_width(ship_length) .translate_center() .rotate(rotation as f64); @@ -76,9 +78,50 @@ fn main() { context.draw_multipolygon(&ship); context.restore(); - // drawing and clipping + // draw score board + let font = AsteroidFont::new(); + let random_seed_string = context.seed.to_string(); + let font_size = 20.; + let (x_font_size, y_font_size) = font.get_text_scale_factor(font_size); + let score_board_letters = random_seed_string.len(); let window = context.frame(frame_offset, frame_offset, frame_offset, frame_offset); - context.draw_polygon(&window); + let window_height = frame_offset + y_font_size; + let window_width = frame_offset + score_board_letters as f64 * x_font_size; + let score_board: Polygon = polygon![(x:0., y:0.), (x: window_width, y:0.), (x: window_width, y: window_height), (x: 0., y: window_height)]; + let score_board_lines = font.get_text(random_seed_string, font_size, FontDirection::LeftRight); + context.save(); + context.translate(frame_offset, frame_offset); + context.draw_line_strings(&score_board_lines); + context.restore(); + + // draw life board + let life = rng.gen_range(2..7) as f64; + let life_board: Polygon = polygon![ + (x:width, y:0.), + (x: width - frame_offset - life * x_font_size - 3., y:0.), + (x: width - frame_offset - life * x_font_size - 3., y:frame_offset + y_font_size + 3.), + (x: width , y:frame_offset + y_font_size + 3.) + ]; + // todo translate_top + // todo translate_left + let life_ship = raw_ship + .scale_to_width(font_size) + .rotate(-90.) + .translate_center_y(); + for l in 1..(1 + life as i32) { + context.save(); + context.translate( + width - frame_offset - (x_font_size * l as f64) as f64, + frame_offset + y_font_size / 2., + ); + context.draw_multipolygon(&life_ship); + context.restore(); + } + + // draw window + let window = window.difference(&score_board, 100.); + let window = window.difference(&life_board, 100.); + context.draw_multipolygon(&window); for asteroid in asteroids.iter() { context.draw_multipolygon(&asteroid.intersection(&window, 100.)); diff --git a/src/context.rs b/src/context.rs index 195dc0e..5b46bec 100644 --- a/src/context.rs +++ b/src/context.rs @@ -19,6 +19,7 @@ pub struct Context { pub width: f64, pub line_size: f64, pub pseudo_random_number_generator: Box, + pub seed: u64, } impl Context { @@ -64,6 +65,12 @@ impl Context { context.stroke(); } + pub fn draw_line_strings(&self, line_strings: &Vec>) { + for lines in line_strings { + self.draw_line_string(&lines); + } + } + /// restore state which was saved before by the context pub fn restore(&self) { self.render_container.context().restore(); @@ -171,6 +178,7 @@ impl Context { line_size, pseudo_random_number_generator: Box::new(rng), render_container, + seed: random_seed, } } } diff --git a/src/font.rs b/src/font.rs index 2b61642..34e5915 100644 --- a/src/font.rs +++ b/src/font.rs @@ -101,6 +101,12 @@ impl AsteroidFont { polygons } + /// size of space (per letter) + pub fn get_text_scale_factor(&self, size: f64) -> (f64, f64) { + let scale_factor = size / 12.; + (10.2 * scale_factor, 14.2 * scale_factor) + } + pub fn get_text( &self, text: String, @@ -111,13 +117,14 @@ impl AsteroidFont { let mut count = 0.; for char in text.chars() { let letter = self.get_letter(&char, size); + let (x_translate_factor, y_translate_factor) = self.get_text_scale_factor(size); for character in letter.iter() { match direction { FontDirection::LeftRight => { - letters.push(character.translate(count * 10.0 * size / 12., 0.)) + letters.push(character.translate(count * x_translate_factor, 0.)) } FontDirection::TopDown => { - letters.push(character.translate(0., count * 14.0 * size / 12.)) + letters.push(character.translate(0., count * y_translate_factor)) } } } From 1574ca725fa86110352a92571eebddd985a4be12 Mon Sep 17 00:00:00 2001 From: Ingolf Wagner Date: Tue, 13 Apr 2021 07:47:29 +0200 Subject: [PATCH 6/8] feat: create translate_ext.rs --- examples/font.rs | 10 +-- examples/svg.rs | 1 + src/bin/asteroids.rs | 12 ++-- src/context.rs | 8 ++- src/extensions.rs | 2 + src/{ => extensions}/multipolygon_ext.rs | 28 -------- src/extensions/translate_ext.rs | 87 ++++++++++++++++++++++++ src/font.rs | 6 +- src/lib.rs | 5 +- src/svg.rs | 6 +- 10 files changed, 117 insertions(+), 48 deletions(-) create mode 100644 src/extensions.rs rename src/{ => extensions}/multipolygon_ext.rs (66%) create mode 100644 src/extensions/translate_ext.rs diff --git a/examples/font.rs b/examples/font.rs index 22b4cc5..6475bdf 100644 --- a/examples/font.rs +++ b/examples/font.rs @@ -11,25 +11,25 @@ fn main() { let font = AsteroidFont::new(); let line = font.get_text("Hey there!".to_string(), 12., LeftRight); - context.draw_line_strings(&line); + context.draw_multiline_string(&line); context.draw_line_string(&line_string![(x : 0., y:14.), (x: 1000. , y : 14.)]); context.translate(0.0, 18.); let line = font.get_text("How are you?".to_string(), 22., LeftRight); - context.draw_line_strings(&line); + context.draw_multiline_string(&line); context.translate(0.0, 28.); let line = font.get_text("Here a bitter text!".to_string(), 50., LeftRight); - context.draw_line_strings(&line); + context.draw_multiline_string(&line); context.translate(0.0, 56.); let line = font.get_text("0123456789".to_string(), 50., LeftRight); - context.draw_line_strings(&line); + context.draw_multiline_string(&line); context.restore(); context.translate(30., 50.); let line = font.get_text("0123456789".to_string(), 30., TopDown); - context.draw_line_strings(&line); + context.draw_multiline_string(&line); context.render(); } diff --git a/examples/svg.rs b/examples/svg.rs index 5543e4d..8b0eef9 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -6,6 +6,7 @@ use geo::{Coordinate, Point}; use polygon_art::load_multipolygon_from_svg; use polygon_art::Context; use polygon_art::MultiPolygonExt; +use polygon_art::TranslateExt; use rand::Rng; fn main() { diff --git a/src/bin/asteroids.rs b/src/bin/asteroids.rs index 9f01cc2..a302754 100644 --- a/src/bin/asteroids.rs +++ b/src/bin/asteroids.rs @@ -6,7 +6,7 @@ use geo::LineString; use geo::{polygon, Coordinate, MultiPolygon, Point, Polygon}; use geo_clipper::Clipper; use polygon_art::{load_multipolygon_from_svg_include, Context}; -use polygon_art::{AsteroidFont, FontDirection, MultiPolygonExt}; +use polygon_art::{AsteroidFont, FontDirection, MultiPolygonExt, TranslateExt}; use rand::prelude::StdRng; use rand::Rng; @@ -91,7 +91,7 @@ fn main() { let score_board_lines = font.get_text(random_seed_string, font_size, FontDirection::LeftRight); context.save(); context.translate(frame_offset, frame_offset); - context.draw_line_strings(&score_board_lines); + context.draw_multiline_string(&score_board_lines); context.restore(); // draw life board @@ -102,17 +102,17 @@ fn main() { (x: width - frame_offset - life * x_font_size - 3., y:frame_offset + y_font_size + 3.), (x: width , y:frame_offset + y_font_size + 3.) ]; - // todo translate_top - // todo translate_left + let life_ship = raw_ship .scale_to_width(font_size) .rotate(-90.) - .translate_center_y(); + .translate_center_y() + .translate_top(); for l in 1..(1 + life as i32) { context.save(); context.translate( width - frame_offset - (x_font_size * l as f64) as f64, - frame_offset + y_font_size / 2., + frame_offset, ); context.draw_multipolygon(&life_ship); context.restore(); diff --git a/src/context.rs b/src/context.rs index 5b46bec..cdc64eb 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,6 @@ use cairo::Format; use cairo::{ImageSurface, SvgSurface}; -use geo::{polygon, LineString, MultiPolygon, Polygon}; +use geo::{polygon, LineString, MultiLineString, MultiPolygon, Polygon}; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; @@ -71,6 +71,12 @@ impl Context { } } + pub fn draw_multiline_string(&self, multilinestring: &MultiLineString) { + for line in multilinestring.iter() { + self.draw_line_string(line); + } + } + /// restore state which was saved before by the context pub fn restore(&self) { self.render_container.context().restore(); diff --git a/src/extensions.rs b/src/extensions.rs new file mode 100644 index 0000000..76f3e22 --- /dev/null +++ b/src/extensions.rs @@ -0,0 +1,2 @@ +pub(crate) mod multipolygon_ext; +pub(crate) mod translate_ext; diff --git a/src/multipolygon_ext.rs b/src/extensions/multipolygon_ext.rs similarity index 66% rename from src/multipolygon_ext.rs rename to src/extensions/multipolygon_ext.rs index f31e86f..8b79290 100644 --- a/src/multipolygon_ext.rs +++ b/src/extensions/multipolygon_ext.rs @@ -4,41 +4,13 @@ use geo::Polygon; use geo_types::{Coordinate, LineString, MultiPolygon}; pub trait MultiPolygonExt { - fn translate_center(&self) -> Self; - fn translate_center_x(&self) -> Self; - fn translate_center_y(&self) -> Self; fn scale_to_width(&self, size: f64) -> Self; fn scale(&self, factor: f64) -> Self; } impl MultiPolygonExt for MultiPolygon { - fn translate_center(&self) -> Self { - let option = self.bounding_rect().unwrap(); - let y_diff = option.max().y - option.min().y; - let y_shift = y_diff / 2.0; - let x_diff = option.max().x - option.min().x; - let x_shift = x_diff / 2.0; - self.translate(-(option.min().x + x_shift), -(option.min().y + y_shift)) - } - - fn translate_center_x(&self) -> Self { - let option = self.bounding_rect().unwrap(); - let x_diff = option.max().x - option.min().x; - let x_shift = x_diff / 2.0; - self.translate(-(option.min().x + x_shift), 0.0) - } - - fn translate_center_y(&self) -> Self { - let option = self.bounding_rect().unwrap(); - let y_diff = option.max().y - option.min().y; - let y_shift = y_diff / 2.0; - self.translate(0.0, -(option.min().y + y_shift)) - } - fn scale_to_width(&self, size: f64) -> Self { - // let ((x_min, y_min), (x_max, _)) = self.get_min_max_rectangle(); let option = self.bounding_rect().unwrap(); - let diff = f64::abs(option.min().x - option.max().x); let scale = size / diff; self.scale(scale) diff --git a/src/extensions/translate_ext.rs b/src/extensions/translate_ext.rs new file mode 100644 index 0000000..1dee99f --- /dev/null +++ b/src/extensions/translate_ext.rs @@ -0,0 +1,87 @@ +use geo::algorithm::bounding_rect::BoundingRect; +use geo::algorithm::translate::Translate; +use geo::{CoordinateType, LineString, MultiLineString, MultiPolygon, Polygon, Rect}; + +/// Translate Extensions +pub trait TranslateExt { + /// translate the object to it's center + fn translate_center(&self) -> Self; + /// translate the object to it's center on the x aches + fn translate_center_x(&self) -> Self; + /// translate the object to it's center on the y aches + fn translate_center_y(&self) -> Self; + /// translate the object, so it touches the y aches + fn translate_left(&self) -> Self; + /// translate the object, so it touches the x aches + fn translate_top(&self) -> Self; +} + +/// wrapper for BoundingRect because of the Output type +pub trait ExplicitBoundingRect +where + T: CoordinateType, +{ + fn bounding(&self) -> Option>; +} + +impl ExplicitBoundingRect for Polygon { + fn bounding(&self) -> Option> { + self.bounding_rect() + } +} + +impl ExplicitBoundingRect for MultiPolygon { + fn bounding(&self) -> Option> { + self.bounding_rect() + } +} + +impl ExplicitBoundingRect for LineString { + fn bounding(&self) -> Option> { + self.bounding_rect() + } +} + +impl ExplicitBoundingRect for MultiLineString { + fn bounding(&self) -> Option> { + self.bounding_rect() + } +} + +impl TranslateExt for B +where + B: ExplicitBoundingRect + Translate, +{ + fn translate_center(&self) -> Self { + let option = self.bounding().unwrap(); + let y_diff = option.max().y - option.min().y; + let y_shift = y_diff / 2.0; + let x_diff = option.max().x - option.min().x; + let x_shift = x_diff / 2.0; + self.translate(-(option.min().x + x_shift), -(option.min().y + y_shift)) + } + + fn translate_center_x(&self) -> Self { + let option = self.bounding().unwrap(); + let x_diff = option.max().x - option.min().x; + let x_shift = x_diff / 2.0; + self.translate(-(option.min().x + x_shift), 0.0) + } + + fn translate_center_y(&self) -> Self { + let option = self.bounding().unwrap(); + let y_diff = option.max().y - option.min().y; + let y_shift = y_diff / 2.0; + self.translate(0.0, -(option.min().y + y_shift)) + } + + fn translate_left(&self) -> Self { + let bounding = self.bounding().unwrap(); + self.translate(-bounding.min().x, 0.) + } + + fn translate_top(&self) -> Self { + let bounding = self.bounding().unwrap(); + self.translate(0., -bounding.min().y) + } +} diff --git a/src/font.rs b/src/font.rs index 34e5915..bbc7aa8 100644 --- a/src/font.rs +++ b/src/font.rs @@ -2,7 +2,7 @@ // https://github.com/osresearch/vst/blob/master/teensyv/asteroids_font.c use geo::algorithm::translate::Translate; -use geo::{Coordinate, LineString}; +use geo::{Coordinate, LineString, MultiLineString}; use std::collections::HashMap; pub struct AsteroidFont { @@ -112,7 +112,7 @@ impl AsteroidFont { text: String, size: f64, direction: FontDirection, - ) -> Vec> { + ) -> MultiLineString { let mut letters: Vec> = Vec::new(); let mut count = 0.; for char in text.chars() { @@ -130,7 +130,7 @@ impl AsteroidFont { } count = count + 1.; } - letters + MultiLineString(letters) } } diff --git a/src/lib.rs b/src/lib.rs index c1aecd4..18a3ad2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,9 @@ mod context; pub use crate::context::Context; -mod multipolygon_ext; -pub use crate::multipolygon_ext::MultiPolygonExt; +mod extensions; +pub use crate::extensions::multipolygon_ext::MultiPolygonExt; +pub use crate::extensions::translate_ext::TranslateExt; mod svg; pub use crate::svg::load_multipolygon_from_svg; diff --git a/src/svg.rs b/src/svg.rs index 287bfae..d3b3158 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -9,7 +9,7 @@ use svg::Parser; pub fn load_multipolygon_from_svg(path: &str) -> Result, Box> { let mut content = String::new(); let parser = svg::open(path, &mut content)?; - parse_svg(parser) + parse_svg_to_multipolygon(parser) } /// load a multipolygon statically at compiletime @@ -18,11 +18,11 @@ pub fn load_multipolygon_from_svg_include( content: &str, ) -> Result, Box> { let parser = svg::read(content)?; - parse_svg(parser) + parse_svg_to_multipolygon(parser) } #[allow(non_upper_case_globals)] -fn parse_svg(parser: Parser) -> Result, Box> { +fn parse_svg_to_multipolygon(parser: Parser) -> Result, Box> { let mut result: Vec> = Vec::new(); for event in parser { match event { From f008f92bfca73e68d20337e8d6b281d5bf82fadd Mon Sep 17 00:00:00 2001 From: Ingolf Wagner Date: Tue, 13 Apr 2021 20:24:04 +0200 Subject: [PATCH 7/8] feat: add merge_ext.rs and scale_ext.rs --- examples/svg.rs | 3 +- src/bin/asteroids.rs | 30 ++++++------- src/extensions.rs | 3 +- src/extensions/merge_ext.rs | 35 +++++++++++++++ src/extensions/multipolygon_ext.rs | 51 ---------------------- src/extensions/scale_ext.rs | 68 ++++++++++++++++++++++++++++++ src/extensions/translate_ext.rs | 28 ++++++------ src/lib.rs | 3 +- 8 files changed, 134 insertions(+), 87 deletions(-) create mode 100644 src/extensions/merge_ext.rs delete mode 100644 src/extensions/multipolygon_ext.rs create mode 100644 src/extensions/scale_ext.rs diff --git a/examples/svg.rs b/examples/svg.rs index 8b0eef9..07f1f71 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -3,10 +3,9 @@ use geo::rotate::RotatePoint; use geo::translate::Translate; use geo::{Coordinate, Point}; -use polygon_art::load_multipolygon_from_svg; use polygon_art::Context; -use polygon_art::MultiPolygonExt; use polygon_art::TranslateExt; +use polygon_art::{load_multipolygon_from_svg, ScaleExt}; use rand::Rng; fn main() { diff --git a/src/bin/asteroids.rs b/src/bin/asteroids.rs index a302754..ff754e1 100644 --- a/src/bin/asteroids.rs +++ b/src/bin/asteroids.rs @@ -5,8 +5,8 @@ use geo::rotate::RotatePoint; use geo::LineString; use geo::{polygon, Coordinate, MultiPolygon, Point, Polygon}; use geo_clipper::Clipper; -use polygon_art::{load_multipolygon_from_svg_include, Context}; -use polygon_art::{AsteroidFont, FontDirection, MultiPolygonExt, TranslateExt}; +use polygon_art::{load_multipolygon_from_svg_include, Context, MergeExt, ScaleExt}; +use polygon_art::{AsteroidFont, FontDirection, TranslateExt}; use rand::prelude::StdRng; use rand::Rng; @@ -19,12 +19,10 @@ fn main() { let height = context.height; let diagonal = f64::sqrt(width * width + height * height); - // todo : it does not make to much sense to define all parameters like this - // todo : it makes more sense to create a static (diagonal / width /height ratio ) setup and scale it up let frame_offset = diagonal * 0.01; - let max_asteroid = (diagonal / 40.) as usize; + let max_asteroid = (diagonal / 80.) as usize; - let min_asteroid_size = diagonal / 40.; + let min_asteroid_size = diagonal / 50.; let max_asteroid_size = diagonal / 12.; let ship_length = diagonal / 23.; let ship_orbit = 3.0; @@ -34,7 +32,7 @@ fn main() { .translate(width / 2., height / 2.); // create asteroid - let mut asteroids: Vec> = Vec::new(); + let mut asteroids: MultiPolygon = MultiPolygon(Vec::new()); for _ in 0..(max_asteroid * 2) { let without_orbit = rng.gen_range(min_asteroid_size..max_asteroid_size) as f64; let with_orbit = without_orbit * 1.32; @@ -44,15 +42,11 @@ fn main() { .scale_to_width(with_orbit) .translate(x, y); - if (!center_point.intersects(&asteroid)) - && asteroids - .iter() - .find(|&m| *&m.intersects(&asteroid)) - .is_none() - { - asteroids.push(asteroid.scale_to_width(without_orbit)); + if (!center_point.intersects(&asteroid)) && !asteroids.intersects(&asteroid) { + // asteroids.push(asteroid.scale_to_width(without_orbit)); + asteroids.merge_destructive(&mut asteroid.scale_to_width(without_orbit)) } - if asteroids.len() > max_asteroid { + if asteroids.0.len() > max_asteroid { break; } } @@ -123,10 +117,10 @@ fn main() { let window = window.difference(&life_board, 100.); context.draw_multipolygon(&window); - for asteroid in asteroids.iter() { - context.draw_multipolygon(&asteroid.intersection(&window, 100.)); - } + // draw asteroids + context.draw_multipolygon(&asteroids.intersection(&window, 100.)); + // render image context.render(); } diff --git a/src/extensions.rs b/src/extensions.rs index 76f3e22..f2166c7 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -1,2 +1,3 @@ -pub(crate) mod multipolygon_ext; +pub(crate) mod merge_ext; +pub(crate) mod scale_ext; pub(crate) mod translate_ext; diff --git a/src/extensions/merge_ext.rs b/src/extensions/merge_ext.rs new file mode 100644 index 0000000..8aab680 --- /dev/null +++ b/src/extensions/merge_ext.rs @@ -0,0 +1,35 @@ +use geo::{LineString, MultiLineString, MultiPolygon}; + +pub trait MergeExt { + /// merge Container object + fn merge(&mut self, other: &Self); + /// merge container object, empty the other object + fn merge_destructive(&mut self, other: &mut Self); +} + +impl MergeExt for LineString { + fn merge(&mut self, other: &Self) { + self.0.append(&mut other.0.clone()); + } + fn merge_destructive(&mut self, other: &mut Self) { + self.0.append(&mut other.0); + } +} + +impl MergeExt for MultiLineString { + fn merge(&mut self, other: &Self) { + self.0.append(&mut other.0.clone()); + } + fn merge_destructive(&mut self, other: &mut Self) { + self.0.append(&mut other.0); + } +} + +impl MergeExt for MultiPolygon { + fn merge(&mut self, other: &Self) { + self.0.append(&mut other.0.clone()); + } + fn merge_destructive(&mut self, other: &mut Self) { + self.0.append(&mut other.0); + } +} diff --git a/src/extensions/multipolygon_ext.rs b/src/extensions/multipolygon_ext.rs deleted file mode 100644 index 8b79290..0000000 --- a/src/extensions/multipolygon_ext.rs +++ /dev/null @@ -1,51 +0,0 @@ -use geo::algorithm::bounding_rect::BoundingRect; -use geo::algorithm::translate::Translate; -use geo::Polygon; -use geo_types::{Coordinate, LineString, MultiPolygon}; - -pub trait MultiPolygonExt { - fn scale_to_width(&self, size: f64) -> Self; - fn scale(&self, factor: f64) -> Self; -} - -impl MultiPolygonExt for MultiPolygon { - fn scale_to_width(&self, size: f64) -> Self { - let option = self.bounding_rect().unwrap(); - let diff = f64::abs(option.min().x - option.max().x); - let scale = size / diff; - self.scale(scale) - } - - fn scale(&self, factor: f64) -> Self { - let bounding_rectangle = self.bounding_rect().unwrap(); - let zeroed_multi_polygons = - self.translate(-bounding_rectangle.min().x, -bounding_rectangle.min().y); - let mut new_polygons: Vec> = Vec::new(); - for polygon in zeroed_multi_polygons.iter() { - // map interiors - let mut interiors: Vec> = Vec::new(); - for interior_line_string in polygon.interiors().to_vec() { - let mut new_line_string: Vec> = Vec::new(); - for point in interior_line_string.into_points() { - new_line_string.push(Coordinate { - x: point.x() * factor, - y: point.y() * factor, - }); - } - interiors.push(LineString::from(new_line_string)); - } - // map exterior - let mut new_line_string: Vec> = Vec::new(); - for point in polygon.exterior().clone().into_points() { - new_line_string.push(Coordinate { - x: point.x() * factor, - y: point.y() * factor, - }); - } - let exterior = LineString::from(new_line_string); - new_polygons.push(Polygon::new(exterior, interiors)); - } - MultiPolygon::from(new_polygons) - .translate(bounding_rectangle.min().x, bounding_rectangle.min().y) - } -} diff --git a/src/extensions/scale_ext.rs b/src/extensions/scale_ext.rs new file mode 100644 index 0000000..dfd159f --- /dev/null +++ b/src/extensions/scale_ext.rs @@ -0,0 +1,68 @@ +use crate::extensions::translate_ext::ExplicitBoundingRect; +use geo::algorithm::map_coords::MapCoords; +use geo::algorithm::translate::Translate; +use geo::{CoordinateType, LineString, MultiLineString, MultiPolygon, Polygon}; + +pub trait ExplicitMapCoords { + fn map_coordinates(&self, func: impl Fn(&(T, T)) -> (T, T) + Copy) -> Self; +} + +impl ExplicitMapCoords for LineString { + fn map_coordinates(&self, func: impl Fn(&(T, T)) -> (T, T) + Copy) -> Self { + self.map_coords(func) + } +} + +impl ExplicitMapCoords for MultiLineString { + fn map_coordinates(&self, func: impl Fn(&(T, T)) -> (T, T) + Copy) -> Self { + self.map_coords(func) + } +} + +impl ExplicitMapCoords for MultiPolygon { + fn map_coordinates(&self, func: impl Fn(&(T, T)) -> (T, T) + Copy) -> Self { + self.map_coords(func) + } +} + +impl ExplicitMapCoords for Polygon { + fn map_coordinates(&self, func: impl Fn(&(T, T)) -> (T, T) + Copy) -> Self { + self.map_coords(func) + } +} + +pub trait ScaleExt { + /// scale object + fn scale(&self, scale_x: f64, scale_y: f64) -> Self; + /// scale object to given width (in 1:1 ratio) + fn scale_to_width(&self, size: f64) -> Self; + /// scale object to given height (in 1:1 ratio) + fn scale_to_height(&self, size: f64) -> Self; +} + +impl ScaleExt for V +where + V: ExplicitBoundingRect + ExplicitMapCoords + Translate, +{ + fn scale(&self, scale_x: f64, scale_y: f64) -> Self { + let bounding_rectangle = self.bounding().unwrap(); + let minimum_coordinates = bounding_rectangle.min(); + self.translate(-minimum_coordinates.x, -minimum_coordinates.y) + .map_coordinates(|&(x, y)| (x * scale_x, y * scale_y)) + .translate(minimum_coordinates.x, minimum_coordinates.y) + } + + fn scale_to_width(&self, size: f64) -> Self { + let option = self.bounding().unwrap(); + let diff = f64::abs(option.min().x - option.max().x); + let scale = size / diff; + self.scale(scale, scale) + } + + fn scale_to_height(&self, size: f64) -> Self { + let option = self.bounding().unwrap(); + let diff = f64::abs(option.min().y - option.max().y); + let scale = size / diff; + self.scale(scale, scale) + } +} diff --git a/src/extensions/translate_ext.rs b/src/extensions/translate_ext.rs index 1dee99f..8d43461 100644 --- a/src/extensions/translate_ext.rs +++ b/src/extensions/translate_ext.rs @@ -2,20 +2,6 @@ use geo::algorithm::bounding_rect::BoundingRect; use geo::algorithm::translate::Translate; use geo::{CoordinateType, LineString, MultiLineString, MultiPolygon, Polygon, Rect}; -/// Translate Extensions -pub trait TranslateExt { - /// translate the object to it's center - fn translate_center(&self) -> Self; - /// translate the object to it's center on the x aches - fn translate_center_x(&self) -> Self; - /// translate the object to it's center on the y aches - fn translate_center_y(&self) -> Self; - /// translate the object, so it touches the y aches - fn translate_left(&self) -> Self; - /// translate the object, so it touches the x aches - fn translate_top(&self) -> Self; -} - /// wrapper for BoundingRect because of the Output type pub trait ExplicitBoundingRect where @@ -48,6 +34,20 @@ impl ExplicitBoundingRect for MultiLineString { } } +/// Translate Extensions +pub trait TranslateExt { + /// translate the object to it's center + fn translate_center(&self) -> Self; + /// translate the object to it's center on the x aches + fn translate_center_x(&self) -> Self; + /// translate the object to it's center on the y aches + fn translate_center_y(&self) -> Self; + /// translate the object, so it touches the y aches + fn translate_left(&self) -> Self; + /// translate the object, so it touches the x aches + fn translate_top(&self) -> Self; +} + impl TranslateExt for B where B: ExplicitBoundingRect + Translate, diff --git a/src/lib.rs b/src/lib.rs index 18a3ad2..6923347 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,8 @@ mod context; pub use crate::context::Context; mod extensions; -pub use crate::extensions::multipolygon_ext::MultiPolygonExt; +pub use crate::extensions::merge_ext::MergeExt; +pub use crate::extensions::scale_ext::ScaleExt; pub use crate::extensions::translate_ext::TranslateExt; mod svg; From de63c1eace9f8324a18099a5f45a0ff117df7023 Mon Sep 17 00:00:00 2001 From: Ingolf Wagner Date: Wed, 14 Apr 2021 21:00:48 +0200 Subject: [PATCH 8/8] doc: release 1.0.0 --- .gitignore | 4 +--- Cargo.toml | 3 ++- README.md | 16 +++++++++++++--- default.nix | 27 --------------------------- images/asteroids.png | Bin 0 -> 29663 bytes 5 files changed, 16 insertions(+), 34 deletions(-) delete mode 100644 default.nix create mode 100644 images/asteroids.png diff --git a/.gitignore b/.gitignore index 5099aff..3bd18d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,7 @@ *.png -*.json *.svg !pool/**/*.svg -!pool/**/*.png -!pool/**/*.json +!images/**/*.png .idea diff --git a/Cargo.toml b/Cargo.toml index 5994e77..48d30dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,9 @@ version = "1.0.0" authors = ["Ingolf Wagner "] edition = "2018" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -description = "Convenience library to use geo polygons to create art." +description = "Convenience library to use geo to create art." documentation = "https://github.com/mrVanDalo/polygon-art" homepage = "https://github.com/mrVanDalo/polygon-art" repository = "https://github.com/mrVanDalo/polygon-art" diff --git a/README.md b/README.md index 0586494..df8f8cb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,22 @@ -Convenience library to create art using [geo](https://georust.org/) -polygons. +Convenience library to create art using [geo](https://georust.org/). * Creates CLI interface (including random seed management) * renders SVGs and PNG -* comes with helper functions, to make live more convenient. +* comes with helper functions and extensions, to make live more convenient. * load SVGs * scaling +# Asteroids + +asteroids is an example binary which renders an +image inspired by the +[asteroids game from the 1979](https://de.wikipedia.org/wiki/Asteroids). + +![image](./images/asteroids.png) + +run `asteroids --help` to get information. +All binaries created by polygon art have the same +command line interface. # How to run examples diff --git a/default.nix b/default.nix deleted file mode 100644 index 8dce5bb..0000000 --- a/default.nix +++ /dev/null @@ -1,27 +0,0 @@ -{ rustPlatform, fetchgit, stdenv, cairo, ... }: - -rustPlatform.buildRustPackage rec { - name = "image-geneartor-${version}"; - version = "3.0.0"; - # src = ./.; - src = fetchgit{ - url = "https://github.com/mrVanDalo/image-generator"; - rev = "f8c1bf958aeea1808df6baea62eeb2949aa5fe65"; - sha256 = "1swa2i57sdag3zapzx3m9mdarci0xfjczr611320zampw505ai09"; - }; - - cargoSha256 = "07pwds279qc54g5fza805ah2m8jhrdzng7vb1bi24a9ra9ci8s29"; - verifyCargoDeps = true; - - buildInputs = [ cairo ]; - - meta = with stdenv.lib; { - description = - "An image generator unsing entropy and a JSON as configuration."; - homepage = "https://github.com/mrVanDalo/image-generator"; - license = licenses.gplv3; - maintainers = [ maintainers.mrVanDalo ]; - platforms = platforms.all; - }; -} - diff --git a/images/asteroids.png b/images/asteroids.png new file mode 100644 index 0000000000000000000000000000000000000000..43cd6a70cd23d717badf90b6bbb9785763b0e2a3 GIT binary patch literal 29663 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 V^xQfW{U2=pT1H8_MAGon{{!ah