this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

✨ Hatched patterns (bottom-up only)

authored by

Gwenn Le Bihan and committed by
Ewen Le Bihan
fbeac66c 474071f8

+216 -90
+3
Justfile
··· 14 14 15 15 example-video args='': 16 16 ./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}} 17 + 18 + example-image args='': 19 + ./shapemaker image --colors colorschemes/palenight.css out.svg {{args}}
+7
src/anchors.rs
··· 9 9 self.0 += dx; 10 10 self.1 += dy; 11 11 } 12 + 13 + pub fn distances(&self, other: &Anchor) -> (usize, usize) { 14 + ( 15 + self.0.abs_diff(other.0) as usize, 16 + self.1.abs_diff(other.1) as usize, 17 + ) 18 + } 12 19 } 13 20 14 21 impl From<(i32, i32)> for Anchor {
+39 -15
src/canvas.rs
··· 4 4 use chrono::DateTime; 5 5 use itertools::Itertools; 6 6 use rand::Rng; 7 + use svg::node::element::Pattern; 7 8 8 9 use crate::{ 9 10 layer::Layer, objects::Object, random_color, web::console_log, Anchor, CenterAnchor, Color, ··· 364 365 self.remove_background() 365 366 } 366 367 367 - pub fn save_as_png( 368 + pub fn save_as( 368 369 at: &str, 369 370 aspect_ratio: f32, 370 371 resolution: usize, ··· 377 378 // portrait: resolution is height 378 379 ((resolution as f32 / aspect_ratio) as usize, resolution) 379 380 }; 381 + 380 382 let mut spawned = std::process::Command::new("magick") 381 383 .args(["-background", "none"]) 382 384 .args(["-size", &format!("{}x{}", width, height)]) ··· 417 419 .collect() 418 420 } 419 421 422 + fn unique_pattern_fills(&self) -> Vec<Fill> { 423 + self.layers 424 + .iter() 425 + .flat_map(|layer| { 426 + layer 427 + .objects 428 + .iter() 429 + .flat_map(|(_, o)| o.1.map(|fill| fill.clone())) 430 + }) 431 + .filter(|fill| matches!(fill, Fill::Hatched(..))) 432 + .unique_by(|fill| fill.pattern_id()) 433 + .collect() 434 + } 435 + 420 436 pub fn render(&mut self, layers: &Vec<&str>, render_background: bool) -> String { 421 437 let background_color = self.background.unwrap_or_default(); 422 438 let mut svg = svg::Document::new(); ··· 427 443 .set("y", -(self.canvas_outter_padding as i32)) 428 444 .set("width", self.width()) 429 445 .set("height", self.height()) 430 - .set("fill", background_color.to_string(&self.colormap)), 446 + .set("fill", background_color.render(&self.colormap)), 431 447 ); 432 448 } 433 449 for layer in self ··· 439 455 svg = svg.add(layer.render(self.colormap.clone(), self.cell_size, layer.object_sizes)); 440 456 } 441 457 458 + let mut defs = svg::node::element::Definitions::new(); 442 459 for filter in self.unique_filters() { 443 - svg = svg.add(filter.definition()) 460 + defs = defs.add(filter.definition()) 461 + } 462 + 463 + for pattern_fill in self.unique_pattern_fills() { 464 + if let Some(patterndef) = pattern_fill.pattern_definition(&self.colormap) { 465 + defs = defs.add(patterndef) 466 + } 444 467 } 445 468 446 - svg.set( 447 - "viewBox", 448 - format!( 449 - "{0} {0} {1} {2}", 450 - -(self.canvas_outter_padding as i32), 451 - self.width(), 452 - self.height() 453 - ), 454 - ) 455 - .set("width", self.width()) 456 - .set("height", self.height()) 457 - .to_string() 469 + svg.add(defs) 470 + .set( 471 + "viewBox", 472 + format!( 473 + "{0} {0} {1} {2}", 474 + -(self.canvas_outter_padding as i32), 475 + self.width(), 476 + self.height() 477 + ), 478 + ) 479 + .set("width", self.width()) 480 + .set("height", self.height()) 481 + .to_string() 458 482 } 459 483 } 460 484
+3 -3
src/color.rs
··· 5 5 path::PathBuf, 6 6 }; 7 7 8 - use serde::Deserialize; 9 8 use rand::Rng; 9 + use serde::Deserialize; 10 10 use wasm_bindgen::prelude::*; 11 11 12 12 #[wasm_bindgen] ··· 72 72 } 73 73 74 74 impl Color { 75 - pub fn to_string(self, mapping: &ColorMapping) -> String { 75 + pub fn render(self, mapping: &ColorMapping) -> String { 76 76 match self { 77 77 Color::Black => mapping.black.to_string(), 78 78 Color::White => mapping.white.to_string(), ··· 259 259 260 260 fn from_css_line(&mut self, line: &str) { 261 261 if let Some((name, value)) = line.trim().split_once(":") { 262 - let value = value.trim().to_owned(); 262 + let value = value.trim().trim_end_matches(";").to_owned(); 263 263 match name.trim() { 264 264 "black" => self.black = value, 265 265 "white" => self.white = value,
+111 -47
src/fill.rs
··· 1 + use std::hash::Hash; 2 + 1 3 use crate::{Color, ColorMapping, RenderCSS}; 4 + 5 + #[derive(Debug, Clone, Copy)] 6 + pub enum Fill { 7 + Solid(Color), 8 + Translucent(Color, f32), 9 + Hatched(Color, HatchDirection, f32, f32), 10 + Dotted(Color, f32), 11 + } 2 12 3 13 #[derive(Debug, Clone, Copy)] 4 14 pub enum HatchDirection { ··· 8 18 TopDownDiagonal, 9 19 } 10 20 11 - impl HatchDirection { 12 - pub fn svg_filter_name(&self) -> String { 13 - "hatch-".to_owned() 14 - + match self { 15 - HatchDirection::Horizontal => "horizontal", 16 - HatchDirection::Vertical => "vertical", 17 - HatchDirection::BottomUpDiagonal => "bottom-up", 18 - HatchDirection::TopDownDiagonal => "top-down", 19 - } 20 - } 21 + const PATTERN_SIZE: usize = 8; 21 22 22 - pub fn svg_pattern_definition(&self) -> String { 23 - // https://stackoverflow.com/a/14500054/9943464 24 - format!( 25 - r#"<pattern id="{}" patternUnits="userSpaceOnUse" width="{}" height="{}">"#, 26 - self.svg_filter_name(), 27 - todo!(), 28 - todo!() 29 - ) + &match self { 30 - HatchDirection::BottomUpDiagonal => format!( 31 - r#"<path 32 - d="M-1,1 l2,-2 33 - M0,4 l4,-4 34 - M3,5 l2,-2" 35 - style="stroke:black; stroke-width:1" 36 - />"# 37 - ), 38 - HatchDirection::Horizontal => todo!(), 39 - HatchDirection::Vertical => todo!(), 40 - HatchDirection::TopDownDiagonal => todo!(), 41 - } + "</pattern>" 42 - } 43 - } 44 - 45 - #[derive(Debug, Clone, Copy)] 46 - pub enum Fill { 47 - Solid(Color), 48 - Translucent(Color, f32), 49 - Hatched(HatchDirection), 50 - Dotted(f32), 51 - } 23 + impl HatchDirection {} 52 24 53 25 impl RenderCSS for Fill { 54 26 fn render_fill_css(&self, colormap: &ColorMapping) -> String { 55 27 match self { 56 28 Fill::Solid(color) => { 57 - format!("fill: {};", color.to_string(colormap)) 29 + format!("fill: {};", color.render(colormap)) 58 30 } 59 31 Fill::Translucent(color, opacity) => { 60 - format!("fill: {}; opacity: {};", color.to_string(colormap), opacity) 32 + format!("fill: {}; opacity: {};", color.render(colormap), opacity) 61 33 } 62 - Fill::Dotted(radius) => unimplemented!(), 63 - Fill::Hatched(direction) => { 64 - format!("fill: url(#{});", direction.svg_filter_name()) 34 + Fill::Dotted(..) => unimplemented!(), 35 + Fill::Hatched(..) => { 36 + format!("fill: url(#{});", self.pattern_id()) 65 37 } 66 38 } 67 39 } ··· 69 41 fn render_stroke_css(&self, colormap: &ColorMapping) -> String { 70 42 match self { 71 43 Fill::Solid(color) => { 72 - format!("stroke: {}; fill: transparent;", color.to_string(colormap)) 44 + format!("stroke: {}; fill: transparent;", color.render(colormap)) 73 45 } 74 46 Fill::Translucent(color, opacity) => { 75 47 format!( 76 48 "stroke: {}; opacity: {}; fill: transparent;", 77 - color.to_string(colormap), 49 + color.render(colormap), 78 50 opacity 79 51 ) 80 52 } 81 53 Fill::Dotted(..) => unimplemented!(), 82 54 Fill::Hatched(..) => unimplemented!(), 55 + } 56 + } 57 + } 58 + 59 + impl Fill { 60 + pub fn pattern_name(&self) -> String { 61 + match self { 62 + Fill::Hatched(_, direction, ..) => format!( 63 + "hatched-{}", 64 + match direction { 65 + HatchDirection::Horizontal => "horizontal", 66 + HatchDirection::Vertical => "vertical", 67 + HatchDirection::BottomUpDiagonal => "bottom-up", 68 + HatchDirection::TopDownDiagonal => "top-down", 69 + } 70 + ), 71 + _ => String::from(""), 72 + } 73 + } 74 + 75 + pub fn pattern_id(&self) -> String { 76 + if let Fill::Hatched(color, _, thickness, spacing) = self { 77 + return format!( 78 + "pattern-{}-{}-{}", 79 + self.pattern_name(), 80 + color.name(), 81 + thickness 82 + ); 83 + } 84 + String::from("") 85 + } 86 + 87 + pub fn pattern_definition( 88 + &self, 89 + colormapping: &ColorMapping, 90 + ) -> Option<svg::node::element::Pattern> { 91 + match self { 92 + Fill::Hatched(color, direction, size, thickness_ratio) => { 93 + let root = svg::node::element::Pattern::new() 94 + .set("id", self.pattern_id()) 95 + .set("patternUnits", "userSpaceOnUse"); 96 + 97 + let thickness = size * (2.0 * thickness_ratio); 98 + // TODO: to re-center when tickness ratio != ½ 99 + let offset = 0.0; 100 + 101 + Some(match direction { 102 + HatchDirection::BottomUpDiagonal => root 103 + // https://stackoverflow.com/a/74205714/9943464 104 + /* 105 + <polygon points="0,0 4,0 0,4" fill="yellow"></polygon> 106 + <polygon points="0,8 8,0 8,4 4,8" fill="yellow"></polygon> 107 + <polygon points="0,4 0,8 8,0 4,0" fill="green"></polygon> 108 + <polygon points="4,8 8,8 8,4" fill="green"></polygon> 109 + */ 110 + .add( 111 + svg::node::element::Polygon::new() 112 + .set( 113 + "points", 114 + format!( 115 + "0,0 {},0 0,{}", 116 + offset + thickness / 2.0, 117 + offset + thickness / 2.0 118 + ), 119 + ) 120 + .set("fill", color.render(colormapping)), 121 + ) 122 + .add( 123 + svg::node::element::Polygon::new() 124 + .set( 125 + "points", 126 + format!( 127 + "0,{} {},0 {},{} {},{}", 128 + offset + size, 129 + offset + size, 130 + offset + size, 131 + offset + thickness / 2.0, 132 + offset + thickness / 2.0, 133 + offset + size, 134 + ), 135 + ) 136 + .set("fill", color.render(colormapping)), 137 + ) 138 + .set("height", size * 2.0) 139 + .set("width", size * 2.0) 140 + .set("viewBox", format!("0,0,{},{}", size, size)), 141 + HatchDirection::Horizontal => todo!(), 142 + HatchDirection::Vertical => todo!(), 143 + HatchDirection::TopDownDiagonal => todo!(), 144 + }) 145 + } 146 + _ => None, 83 147 } 84 148 } 85 149 }
+1 -1
src/lib.rs
··· 332 332 aspect_ratio: f32, 333 333 resolution: usize, 334 334 ) -> Result<(), String> { 335 - Canvas::save_as_png( 335 + Canvas::save_as( 336 336 &format!( 337 337 "{}/{:0width$}.png", 338 338 frames_output_directory,
+34 -10
src/main.rs
··· 1 1 use itertools::Itertools; 2 - use shapemaker::{cli::{canvas_from_cli, cli_args}, *}; 2 + use shapemaker::{ 3 + cli::{canvas_from_cli, cli_args}, 4 + *, 5 + }; 3 6 4 7 pub fn main() { 5 8 run(cli_args()); ··· 9 12 let mut canvas = canvas_from_cli(&args); 10 13 11 14 if args.cmd_image && !args.cmd_video { 12 - canvas.layers.push(canvas.random_layer("root")); 15 + canvas.layers.push(Layer::new("root")); 13 16 canvas.set_background(Color::White); 17 + canvas.layer("root").add_object( 18 + "feur", 19 + Object::Rectangle(Anchor(0, 0), Anchor(2, 2)), 20 + Some(Fill::Hatched( 21 + Color::Red, 22 + HatchDirection::BottomUpDiagonal, 23 + 2.0, 24 + 0.25, 25 + )), 26 + ); 27 + // canvas.layers[0].paint_all_objects(Fill::Hatched( 28 + // Color::Red, 29 + // HatchDirection::BottomUpDiagonal, 30 + // 3.0, 31 + // )); 14 32 let aspect_ratio = canvas.grid_size.0 as f32 / canvas.grid_size.1 as f32; 15 - match Canvas::save_as_png( 16 - &args.arg_file, 17 - aspect_ratio, 18 - args.flag_resolution.unwrap_or(1000), 19 - canvas.render(&vec!["*"], true), 20 - ) { 21 - Ok(_) => println!("Image saved to {}", args.arg_file), 22 - Err(e) => println!("Error saving image: {}", e), 33 + 34 + let rendered = canvas.render(&vec!["*"], true); 35 + if args.arg_file.ends_with(".svg") { 36 + std::fs::write(args.arg_file, rendered).unwrap(); 37 + } else { 38 + match Canvas::save_as( 39 + &args.arg_file, 40 + aspect_ratio, 41 + args.flag_resolution.unwrap_or(1000), 42 + rendered, 43 + ) { 44 + Ok(_) => println!("Image saved to {}", args.arg_file), 45 + Err(e) => println!("Error saving image: {}", e), 46 + } 23 47 } 24 48 return; 25 49 }
+6 -7
src/objects.rs
··· 1 1 use crate::{Anchor, CenterAnchor, ColorMapping, Coordinates, Fill, Filter, Point, Region}; 2 2 use itertools::Itertools; 3 + use svg::Node; 3 4 use wasm_bindgen::prelude::*; 4 5 5 6 #[derive(Debug, Clone, PartialEq, Eq)] ··· 170 171 Object::RawSVG(..) => self.render_raw_svg(), 171 172 }; 172 173 173 - group = group.add(rendered); 174 - 175 174 let mut css = String::new(); 176 175 if !matches!(self, Object::RawSVG(..)) { 177 176 css = fill.render_css(colormap, !self.fillable()); ··· 184 183 .join(" ") 185 184 .as_ref(); 186 185 187 - group.set("data-object", id).set("style", css) 186 + group.set("data-object", id).add(rendered).set("style", css) 188 187 } 189 188 190 189 fn render_raw_svg(&self) -> Box<dyn svg::node::Node> { ··· 215 214 if let Object::Rectangle(start, end) = self { 216 215 return Box::new( 217 216 svg::node::element::Rectangle::new() 218 - .set("x1", start.coords(cell_size).0) 219 - .set("y1", start.coords(cell_size).1) 220 - .set("x2", end.coords(cell_size).0) 221 - .set("y2", end.coords(cell_size).1), 217 + .set("x", start.coords(cell_size).0) 218 + .set("y", start.coords(cell_size).1) 219 + .set("width", start.distances(end).0 * cell_size) 220 + .set("height", start.distances(end).1 * cell_size), 222 221 ); 223 222 } 224 223
+1 -1
src/preview.rs
··· 24 24 "frames":frames, 25 25 "audiopath": path_to_audio_file, 26 26 "enginesource": engine_js_source, 27 - "background": canvas.background.map_or("black".to_string(), |color| color.to_string(&canvas.colormap)), 27 + "background": canvas.background.map_or("black".to_string(), |color| color.render(&canvas.colormap)), 28 28 "serverorigin": format!("http://localhost:{}", port), 29 29 "framesbuffersize": FRAMES_BUFFER_SIZE, 30 30 }),
+8 -3
src/web.rs
··· 6 6 use wasm_bindgen::{JsValue, UnwrapThrowExt}; 7 7 8 8 use crate::{ 9 - layer, Anchor, Canvas, CenterAnchor, Color, ColorMapping, Fill, Filter, FilterType, Layer, 10 - Object, 9 + layer, Anchor, Canvas, CenterAnchor, Color, ColorMapping, Fill, Filter, FilterType, 10 + HatchDirection, Layer, Object, 11 11 }; 12 12 13 13 static WEB_CANVAS: Lazy<Mutex<Canvas>> = Lazy::new(|| Mutex::new(Canvas::default_settings())); ··· 61 61 canvas.set_grid_size(4, 4); 62 62 63 63 let mut layer = canvas.random_layer(&color.name()); 64 - layer.paint_all_objects(Fill::Translucent(color.into(), opacity)); 64 + layer.paint_all_objects(Fill::Hatched( 65 + color.into(), 66 + HatchDirection::BottomUpDiagonal, 67 + opacity, 68 + opacity, 69 + )); 65 70 // layer.filter_all_objects(Filter::glow(3.0)); 66 71 canvas.add_or_replace_layer(layer); 67 72
+3 -3
web/index.html
··· 23 23 window.renderImage = (vel, col) => { 24 24 document 25 25 .querySelectorAll(`.frame[data-color=${color_name(col)}]`) 26 - ?.forEach((el) => el.remove()) 26 + ?.forEach((el) => fadeOutElement(el, 200)) 27 27 render_image(vel, col) 28 28 } 29 29 ··· 54 54 return colors[random] 55 55 } 56 56 57 - function fadeOutElement(el) { 57 + function fadeOutElement(el, durationOverride = null) { 58 58 if (!el) return 59 - const duration = window.pedal_held ? 5e3 : 200 59 + const duration = durationOverride ?? (window.pedal_held ? 5e3 : 200) 60 60 el.style.transition = `opacity ${duration}ms ease-out` 61 61 el.style.opacity = 0 62 62 setTimeout(() => el.remove(), duration)