diff --git a/.gitignore b/.gitignore index 5d3e3b4..9a46311 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ fixtures/ preview.html out.png stems_data/ +street diff --git a/Justfile b/Justfile index 8996164..f1159c0 100644 --- a/Justfile +++ b/Justfile @@ -18,4 +18,4 @@ example-video out="out.mp4" args='': ./shapemaker video --colors colorschemes/palenight.css {{out}} --sync-with fixtures/schedule-hell.midi --audio fixtures/schedule-hell.flac --grid-size 16x10 --resolution 1920 {{args}} example-image out="out.png" args='': - ./shapemaker image --colors colorschemes/palenight.css --resolution 3000 {{out}} {{args}} + ./shapemaker image --colors colorschemes/snazzy-light.json --resolution 3000 {{out}} {{args}} diff --git a/src/canvas.rs b/src/canvas.rs index 4aed98f..64c5ab8 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -219,14 +219,7 @@ impl Canvas { let hatchable = object.hatchable(); objects.insert( format!("{}#{}", name, i), - ColoredObject::from(( - object, - if rand::thread_rng().gen_bool(0.5) { - Some(self.random_fill(hatchable)) - } else { - None - }, - )), + object.color(self.random_fill(hatchable)), ); } Layer { @@ -380,6 +373,16 @@ impl Canvas { region.start == (0, 0) && region.end == self.grid_size } + pub fn random_region(&self) -> Region { + let start = self.random_point(&self.world_region); + let end = self.random_end_anchor(start, &self.world_region); + Region::from(if start.0 > end.0 { + (end, start) + } else { + (start, end) + }) + } + pub fn random_point(&self, region: &Region) -> Point { region.ensure_nonempty().unwrap(); Point( @@ -391,11 +394,11 @@ impl Canvas { pub fn random_fill(&self, hatchable: bool) -> Fill { if hatchable { match rand::thread_rng().gen_range(1..=2) { - 1 => Fill::Solid(random_color()), + 1 => Fill::Solid(random_color(self.background)), 2 => { let hatch_size = rand::thread_rng().gen_range(5..=100) as f32 * 1e-2; Fill::Hatched( - random_color(), + random_color(self.background), Angle(rand::thread_rng().gen_range(0.0..360.0)), hatch_size, // under a certain hatch size, we can't see the hatching if the ratio is not ½ @@ -409,7 +412,7 @@ impl Canvas { _ => unreachable!(), } } else { - Fill::Solid(random_color()) + Fill::Solid(random_color(self.background)) } } diff --git a/src/color.rs b/src/color.rs index 814356a..62cf0ba 100644 --- a/src/color.rs +++ b/src/color.rs @@ -27,22 +27,30 @@ pub enum Color { } #[wasm_bindgen] -pub 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!(), - } +pub fn random_color(except: Option) -> Color { + let all = [ + Color::Black, + Color::White, + Color::Red, + Color::Green, + Color::Blue, + Color::Yellow, + Color::Orange, + Color::Purple, + Color::Brown, + Color::Cyan, + Color::Pink, + Color::Gray, + ]; + let candidates = all + .iter() + .filter(|c| match except { + None => true, + Some(color) => &&color != c, + }) + .collect::>(); + + *candidates[rand::thread_rng().gen_range(0..candidates.len())] } impl Default for Color { diff --git a/src/main.rs b/src/main.rs index b1826be..9645824 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,8 @@ +use std::env; + use anyhow::Result; +use itertools::Itertools; +use rand::Rng; use shapemaker::{ cli::{canvas_from_cli, cli_args}, *, @@ -11,8 +15,32 @@ pub fn main() -> Result<()> { pub fn run(args: cli::Args) -> Result<()> { let mut canvas = canvas_from_cli(&args); + let qrname = env::var("QRCODE_NAME").unwrap(); + if args.cmd_image && !args.cmd_video { - canvas = examples::dna_analysis_machine(); + canvas.set_grid_size(3, 3); + canvas.add_or_replace_layer(canvas.random_layer("root")); + canvas.new_layer("qr"); + let qrcode = Object::Image( + vec![ + canvas.world_region.topleft(), + canvas.world_region.topright(), + canvas.world_region.bottomright(), + canvas.world_region.bottomleft(), + ][rand::thread_rng().gen_range(0..4)] + .region(), + format!("./{qrname}-qr.png"), + ); + canvas.root().remove_all_objects_in(&qrcode.region()); + canvas.set_background(Color::White); + canvas.add_object("qr", "qr", qrcode, None).unwrap(); + canvas.put_layer_on_top("qr"); + canvas.root().objects.values_mut().for_each(|o| { + if !o.object.fillable() { + o.fill = Some(Fill::Solid(Color::Black)); + } + }); + let rendered = canvas.render(true)?; if args.arg_file.ends_with(".svg") { std::fs::write(args.arg_file, rendered).unwrap(); diff --git a/src/objects.rs b/src/objects.rs index 0c4cdb1..587df92 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{cell, collections::HashMap}; use crate::{ColorMapping, Fill, Filter, Point, Region, Transformation}; use itertools::Itertools; @@ -24,6 +24,7 @@ pub enum Object { CenteredText(Point, String, f32), // FittedText(Region, String), Rectangle(Point, Point), + Image(Region, String), RawSVG(Box), } @@ -252,6 +253,7 @@ impl Object { | Object::Dot(anchor) | Object::SmallCircle(anchor) => anchor.translate(dx, dy), Object::BigCircle(center) => center.translate(dx, dy), + Object::Image(region, ..) => region.translate(dx, dy), Object::RawSVG(_) => { unimplemented!() } @@ -297,6 +299,7 @@ impl Object { | Object::Dot(anchor) | Object::SmallCircle(anchor) => anchor.region(), Object::BigCircle(center) => center.region(), + Object::Image(region, ..) => *region, Object::RawSVG(_) => { unimplemented!() } @@ -333,12 +336,29 @@ impl Object { Object::SmallCircle(..) => self.render_small_circle(cell_size, object_sizes), Object::Dot(..) => self.render_dot(cell_size, object_sizes), Object::BigCircle(..) => self.render_big_circle(cell_size), + Object::Image(..) => self.render_image(cell_size), Object::RawSVG(..) => self.render_raw_svg(), }; group.set("data-object", id).add(rendered) } + fn render_image(&self, cell_size: usize) -> Box { + if let Object::Image(region, path) = self { + let (x, y) = region.start.coords(cell_size); + return Box::new( + svg::node::element::Image::new() + .set("x", x) + .set("y", y) + .set("width", region.width() * cell_size) + .set("height", region.height() * cell_size) + .set("href", path.clone()), + ); + } + + panic!("Expected Image, got {:?}", self); + } + fn render_raw_svg(&self) -> Box { if let Object::RawSVG(svg) = self { return svg.clone(); diff --git a/src/region.rs b/src/region.rs index 705c647..a40d44a 100644 --- a/src/region.rs +++ b/src/region.rs @@ -208,9 +208,9 @@ impl Region { // panics if the region is invalid pub fn ensure_valid(self) -> Result { - if self.start.0 >= self.end.0 || self.start.1 >= self.end.1 { + if self.start.0 > self.end.0 || self.start.1 > self.end.1 { return Err(format_err!( - "Invalid region: start ({:?}) >= end ({:?})", + "Invalid region: start ({:?}) > end ({:?})", self.start, self.end )); diff --git a/street.fish b/street.fish new file mode 100644 index 0000000..6e37aec --- /dev/null +++ b/street.fish @@ -0,0 +1,13 @@ +while true + set id (nanoid -s 10) + qrencode "https://shapemaker.ewen.works/found/$id" -o street/$id-qr.png + QRCODE_NAME=street/$id just example-image out.svg "--objects-count 5..15" + if test (read || echo "n") = "y" + cp out.svg street/$id.svg + resvg --width 2000 out.svg street/$id.png + echo resvg --width 2000 street/$id.svg street/$id.png + echo saved street/$id.svg \| street/$id.png + else + rm street/$id-qr.png + end +end