diff --git a/Justfile b/Justfile index e219acd..eb85f60 100644 --- a/Justfile +++ b/Justfile @@ -12,8 +12,8 @@ start-web: install: cp shapemaker ~/.local/bin/ -example-video args='': - ./shapemaker video --colors colorschemes/palenight.css out.mp4 --sync-with fixtures/schedule-hell.midi --audio fixtures/schedule-hell.flac --grid-size 16x10 --resolution 1920 {{args}} +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 args='': - ./shapemaker image --colors colorschemes/palenight.css out.svg {{args}} +example-image out="out.png" args='': + ./shapemaker image --colors colorschemes/palenight.css {{out}} {{args}} diff --git a/out.png b/out.png new file mode 100644 index 0000000..be41281 Binary files /dev/null and b/out.png differ diff --git a/src/anchors.rs b/src/anchors.rs index 9a050d4..0da0817 100644 --- a/src/anchors.rs +++ b/src/anchors.rs @@ -10,6 +10,10 @@ impl Anchor { self.1 += dy; } + pub fn translated(&self, dx: i32, dy: i32) -> Self { + Anchor(self.0 + dx, self.1 + dy) + } + pub fn distances(&self, other: &Anchor) -> (usize, usize) { ( self.0.abs_diff(other.0) as usize, @@ -28,6 +32,18 @@ impl From<(i32, i32)> for Anchor { #[wasm_bindgen] pub struct CenterAnchor(pub i32, pub i32); +impl From<(usize, usize)> for CenterAnchor { + fn from(value: (usize, usize)) -> Self { + CenterAnchor(value.0 as i32, value.1 as i32) + } +} + +impl From<(usize, usize)> for Anchor { + fn from(value: (usize, usize)) -> Self { + Anchor(value.0 as i32, value.1 as i32) + } +} + impl CenterAnchor { pub fn translate(&mut self, dx: i32, dy: i32) { self.0 += dx; diff --git a/src/canvas.rs b/src/canvas.rs index ae8e6c5..6f98502 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -8,7 +8,8 @@ use svg::node::element::Pattern; use crate::{ layer::Layer, objects::Object, random_color, web::console_log, Anchor, CenterAnchor, Color, - ColorMapping, ColoredObject, Fill, Filter, LineSegment, ObjectSizes, Point, Region, + ColorMapping, ColoredObject, Fill, Filter, HatchDirection, LineSegment, ObjectSizes, Point, + Region, }; #[derive(Debug, Clone)] @@ -170,12 +171,13 @@ impl Canvas { let number_of_objects = rand::thread_rng().gen_range(self.objects_count_range.clone()); for i in 0..number_of_objects { let object = self.random_object_within(region); + let hatchable = object.hatchable(); objects.insert( format!("{}#{}", name, i), ColoredObject( object, if rand::thread_rng().gen_bool(0.5) { - Some(self.random_fill()) + Some(self.random_fill(hatchable)) } else { None }, @@ -200,12 +202,13 @@ impl Canvas { let number_of_objects = rand::thread_rng().gen_range(self.objects_count_range.clone()); for i in 0..number_of_objects { let object = self.random_linelike_within(region); + let hatchable = object.fillable(); objects.insert( format!("{}#{}", layer_name, i), ColoredObject( object, if rand::thread_rng().gen_bool(0.5) { - Some(self.random_fill()) + Some(self.random_fill(hatchable)) } else { None }, @@ -350,14 +353,29 @@ impl Canvas { } } - pub fn random_fill(&self) -> Fill { - Fill::Solid(random_color()) - // match rand::thread_rng().gen_range(1..=3) { - // 1 => Fill::Solid(random_color()), - // 2 => Fill::Hatched, - // 3 => Fill::Dotted, - // _ => unreachable!(), - // } + pub fn random_fill(&self, hatchable: bool) -> Fill { + if hatchable { + match rand::thread_rng().gen_range(1..=2) { + 1 => Fill::Solid(random_color()), + 2 => { + let hatch_size = rand::thread_rng().gen_range(5..=100) as f32 * 1e-2; + Fill::Hatched( + random_color(), + HatchDirection::BottomUpDiagonal, + hatch_size, + // under a certain hatch size, we can't see the hatching if the ratio is not ½ + if hatch_size < 8.0 { + 0.5 + } else { + rand::thread_rng().gen_range(1..=4) as f32 / 4.0 + }, + ) + } + _ => unreachable!(), + } + } else { + Fill::Solid(random_color()) + } } pub fn clear(&mut self) { diff --git a/src/layer.rs b/src/layer.rs index ee50858..a72b3d0 100644 --- a/src/layer.rs +++ b/src/layer.rs @@ -1,4 +1,5 @@ use crate::{ColorMapping, ColoredObject, Fill, Filter, Object, ObjectSizes}; +use anyhow::Context; use std::collections::HashMap; use wasm_bindgen::prelude::*; @@ -56,19 +57,30 @@ impl Layer { self.flush(); } - pub fn add_object(&mut self, name: &str, object: Object, fill: Option) { - self.objects.insert(name.to_string(), (object, fill).into()); + pub fn add_object(&mut self, name: &str, object: ColoredObject) { + self.objects.insert(name.to_string(), object); self.flush(); } + pub fn filter_object(&mut self, name: &str, filter: Filter) -> Result<(), String> { + self.objects + .get_mut(name) + .ok_or(format!("Object '{}' not found", name))? + .2 + .push(filter); + + self.flush(); + Ok(()) + } + pub fn remove_object(&mut self, name: &str) { self.objects.remove(name); self.flush(); } - pub fn replace_object(&mut self, name: &str, object: Object, fill: Option) { + pub fn replace_object(&mut self, name: &str, object: ColoredObject) { self.remove_object(name); - self.add_object(name, object, fill); + self.add_object(name, object); } /// Render the layer to a SVG group element. diff --git a/src/main.rs b/src/main.rs index fe2a585..0be3f96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use rand::Rng; use shapemaker::{ cli::{canvas_from_cli, cli_args}, *, @@ -12,18 +13,49 @@ pub fn run(args: cli::Args) { let mut canvas = canvas_from_cli(&args); if args.cmd_image && !args.cmd_video { - canvas.layers.push(Layer::new("root")); - canvas.set_background(Color::White); - canvas.layer("root").add_object( - "feur", - Object::Rectangle(Anchor(0, 0), Anchor(2, 2)), - Some(Fill::Hatched( - Color::Red, - HatchDirection::BottomUpDiagonal, - 2.0, - 0.25, - )), - ); + let mut layer = Layer::new("root"); + + let red_circle_at = canvas.world_region.enlarged(-1, -1).random_point_within(); + + for (i, Point(x, y)) in canvas + .world_region + .resized(-1, -1) + .enlarged(-2, -2) + .iter() + .enumerate() + { + layer.add_object( + &format!("{}-{}", x, y), + if rand::thread_rng().gen_bool(0.5) && red_circle_at != Point(x, y) { + Object::BigCircle((x, y).into()) + } else { + Object::Rectangle((x, y).into(), Anchor::from((x, y)).translated(1, 1)) + } + .color(if red_circle_at == Point(x, y) { + Fill::Solid(Color::Red) + } else { + Fill::Hatched( + Color::White, + HatchDirection::BottomUpDiagonal, + (i + 1) as f32 / 10.0, + 0.25, + ) + }), + // .filter(Filter::glow(7.0)), + ); + } + canvas.layers.push(layer); + canvas.set_background(Color::Black); + // canvas.layer("root").add_object( + // "feur", + // Object::Rectangle(Anchor(0, 0), Anchor(2, 2)), + // Some(Fill::Hatched( + // Color::Red, + // HatchDirection::BottomUpDiagonal, + // 2.0, + // 0.25, + // )), + // ); // canvas.layers[0].paint_all_objects(Fill::Hatched( // Color::Red, // HatchDirection::BottomUpDiagonal, @@ -63,21 +95,21 @@ pub fn run(args: cli::Args) { let mut kicks = Layer::new("anchor kick"); - let fill = Some(Fill::Translucent(Color::White, 0.0)); + let fill = Fill::Translucent(Color::White, 0.0); let circle_at = |x: usize, y: usize| Object::SmallCircle(Anchor(x as i32, y as i32)); let (end_x, end_y) = { let Point(x, y) = canvas.world_region.end; (x - 2, y - 2) }; - kicks.add_object("top left", circle_at(1, 1), fill); - kicks.add_object("top right", circle_at(end_x, 1), fill); - kicks.add_object("bottom left", circle_at(1, end_y), fill); - kicks.add_object("bottom right", circle_at(end_x, end_y), fill); + kicks.add_object("top left", circle_at(1, 1).color(fill)); + kicks.add_object("top right", circle_at(end_x, 1).color(fill)); + kicks.add_object("bottom left", circle_at(1, end_y).color(fill)); + kicks.add_object("bottom right", circle_at(end_x, end_y).color(fill)); canvas.add_or_replace_layer(kicks); let mut ch = Layer::new("ch"); - ch.add_object("0", Object::Dot(Anchor(0, 0)), None); + ch.add_object("0", Object::Dot(Anchor(0, 0)).into()); canvas.add_or_replace_layer(ch); }) .sync_audio_with(&args.flag_sync_with.unwrap()) @@ -160,8 +192,8 @@ pub fn run(args: cli::Args) { let object_name = format!("{}", ctx.ms); layer.add_object( &object_name, - Object::Dot(world.resized(-1, -1).random_coordinates_within().into()), - Some(Fill::Solid(Color::Cyan)), + Object::Dot(world.resized(-1, -1).random_coordinates_within().into()) + .color(Fill::Solid(Color::Cyan)), ); canvas.put_layer_on_top("ch"); @@ -170,8 +202,7 @@ pub fn run(args: cli::Args) { .when_remaining(10, &|canvas, _| { canvas.root().add_object( "credits text", - Object::RawSVG(Box::new(svg::node::Text::new("by ewen-lbh"))), - None, + Object::RawSVG(Box::new(svg::node::Text::new("by ewen-lbh"))).into(), ); }) .command("remove", &|argumentsline, canvas, _| { diff --git a/src/objects.rs b/src/objects.rs index e1f1622..feb6c11 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -24,9 +24,36 @@ pub enum Object { RawSVG(Box), } +impl Object { + pub fn color(self, fill: Fill) -> ColoredObject { + ColoredObject::from((self, Some(fill))) + } + + pub fn filter(self, filter: Filter) -> ColoredObject { + ColoredObject::from((self, None)).filter(filter) + } +} + #[derive(Debug, Clone)] pub struct ColoredObject(pub Object, pub Option, pub Vec); +impl ColoredObject { + pub fn filter(mut self, filter: Filter) -> Self { + self.2.push(filter); + self + } + + pub fn clear_filters(&mut self) { + self.2.clear(); + } +} + +impl From for ColoredObject { + fn from(value: Object) -> Self { + ColoredObject(value, None, vec![]) + } +} + impl From<(Object, Option)> for ColoredObject { fn from(value: (Object, Option)) -> Self { ColoredObject(value.0, value.1, vec![]) @@ -148,6 +175,10 @@ impl Object { ) } + pub fn hatchable(&self) -> bool { + self.fillable() && !matches!(self, Object::Dot(..)) + } + pub fn render( &self, cell_size: usize, diff --git a/src/region.rs b/src/region.rs index 2998059..dfc9233 100644 --- a/src/region.rs +++ b/src/region.rs @@ -32,6 +32,47 @@ pub struct Region { pub end: Point, } +impl Region { + pub fn iter(&self) -> RegionIterator { + self.into() + } + + pub fn random_point_within(&self) -> Point { + Point::from(self.random_coordinates_within()) + } +} + +pub struct RegionIterator { + region: Region, + current: Point, +} + +impl Iterator for RegionIterator { + type Item = Point; + + fn next(&mut self) -> Option { + if self.current.0 >= self.region.end.0 { + self.current.0 = self.region.start.0; + self.current.1 += 1; + } + if self.current.1 >= self.region.end.1 { + return None; + } + let result = self.current; + self.current.0 += 1; + Some(result) + } +} + +impl From<&Region> for RegionIterator { + fn from(region: &Region) -> Self { + Self { + region: region.clone(), + current: region.start.clone(), + } + } +} + impl From<((usize, usize), (usize, usize))> for Region { fn from(value: ((usize, usize), (usize, usize))) -> Self { Region { diff --git a/src/web.rs b/src/web.rs index 11b4337..6783d94 100644 --- a/src/web.rs +++ b/src/web.rs @@ -2,12 +2,13 @@ use std::ptr::NonNull; use std::sync::Mutex; use once_cell::sync::Lazy; +use rand::Rng; use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen}; use wasm_bindgen::{JsValue, UnwrapThrowExt}; use crate::{ layer, Anchor, Canvas, CenterAnchor, Color, ColorMapping, Fill, Filter, FilterType, - HatchDirection, Layer, Object, + HatchDirection, Layer, Object, Point, }; static WEB_CANVAS: Lazy> = Lazy::new(|| Mutex::new(Canvas::default_settings())); @@ -57,28 +58,43 @@ pub fn render_image(opacity: f32, color: Color) -> Result<(), JsValue> { gray: "#81a0a8".into(), cyan: "#4fecec".into(), }; + canvas.set_grid_size(16, 9); - canvas.set_grid_size(4, 4); + let mut layer = Layer::new("root"); - let mut layer = canvas.random_layer(&color.name()); - layer.paint_all_objects(Fill::Hatched( - color.into(), - HatchDirection::BottomUpDiagonal, - opacity, - opacity, - )); - // layer.filter_all_objects(Filter::glow(3.0)); - canvas.add_or_replace_layer(layer); + let draw_in = canvas.world_region.resized(-1, -1).enlarged(-2, -2); + let red_circle_at = draw_in.random_point_within(); - let window = web_sys::window().expect("no global `window` exists"); - let document = window.document().expect("should have a document on window"); - let body = document.body().expect("document should have a body"); + console_log!("Red circle at {:?}", red_circle_at); + + for (i, Point(x, y)) in draw_in.iter().enumerate() { + console_log!("Adding object at ({}, {})", x, y); + layer.add_object( + &format!("{}-{}", x, y), + if rand::thread_rng().gen_bool(0.5) && red_circle_at != Point(x, y) { + Object::BigCircle((x, y).into()) + } else { + Object::Rectangle((x, y).into(), Anchor::from((x, y)).translated(1, 1)) + } + .color(if red_circle_at == Point(x, y) { + Fill::Solid(Color::Red) + } else { + Fill::Hatched( + Color::White, + HatchDirection::BottomUpDiagonal, + (i + 1) as f32 / 10.0, + 0.25, + ) + }), + // .filter(Filter::glow(7.0)), + ); + } + console_log!("Registering layer"); + canvas.layers.push(layer); + canvas.set_background(Color::Black); + *WEB_CANVAS.lock().unwrap() = canvas; + render_canvas_at(String::from("body")); - let output = document.create_element("div")?; - output.set_class_name("frame"); - output.set_attribute("data-color", &color.name())?; - output.set_inner_html(&canvas.render(&vec!["*"], false)); - body.append_child(&output)?; Ok(()) } @@ -226,6 +242,8 @@ pub struct LayerWeb { pub name: String, } +// #[wasm_bindgen()] + #[wasm_bindgen] impl LayerWeb { pub fn render(&self) -> String { @@ -265,8 +283,11 @@ impl LayerWeb { ) -> () { canvas().layer(name).add_object( name, - Object::Line(start, end, thickness), - Some(Fill::Solid(color)), + ( + Object::Line(start, end, thickness), + Some(Fill::Solid(color)), + ) + .into(), ) } pub fn new_curve_outward( @@ -279,8 +300,7 @@ impl LayerWeb { ) -> () { canvas().layer(name).add_object( name, - Object::CurveOutward(start, end, thickness), - Some(Fill::Solid(color)), + Object::CurveOutward(start, end, thickness).color(Fill::Solid(color)), ) } pub fn new_curve_inward( @@ -293,24 +313,23 @@ impl LayerWeb { ) -> () { canvas().layer(name).add_object( name, - Object::CurveInward(start, end, thickness), - Some(Fill::Solid(color)), + Object::CurveInward(start, end, thickness).color(Fill::Solid(color)), ) } pub fn new_small_circle(&self, name: &str, center: Anchor, color: Color) -> () { canvas() .layer(name) - .add_object(name, Object::SmallCircle(center), Some(Fill::Solid(color))) + .add_object(name, Object::SmallCircle(center).color(Fill::Solid(color))) } pub fn new_dot(&self, name: &str, center: Anchor, color: Color) -> () { canvas() .layer(name) - .add_object(name, Object::Dot(center), Some(Fill::Solid(color))) + .add_object(name, Object::Dot(center).color(Fill::Solid(color))) } pub fn new_big_circle(&self, name: &str, center: CenterAnchor, color: Color) -> () { canvas() .layer(name) - .add_object(name, Object::BigCircle(center), Some(Fill::Solid(color))) + .add_object(name, Object::BigCircle(center).color(Fill::Solid(color))) } pub fn new_text( &self, @@ -322,8 +341,7 @@ impl LayerWeb { ) -> () { canvas().layer(name).add_object( name, - Object::Text(anchor, text, font_size), - Some(Fill::Solid(color)), + Object::Text(anchor, text, font_size).color(Fill::Solid(color)), ) } pub fn new_rectangle( @@ -335,8 +353,7 @@ impl LayerWeb { ) -> () { canvas().layer(name).add_object( name, - Object::Rectangle(topleft, bottomright), - Some(Fill::Solid(color)), + Object::Rectangle(topleft, bottomright).color(Fill::Solid(color)), ) } }