this repo has no description
3
fork

Configure Feed

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

✨ Allow loading colormaps from css files, and other stuff

authored by

Gwenn Le Bihan and committed by
Ewen Le Bihan
4ddbcd67 feec5e80

+285 -179
+1 -1
Justfile
··· 6 6 cp shapemaker ~/.local/bin/ 7 7 8 8 example-video: 9 - ./shapemaker video --colors colorschemes/afterglow.json out.mp4 --sync-with fixtures/schedule-hell.midi --audio fixtures/schedule-hell.flac --grid-size 16x9 9 + ./shapemaker video --colors colorschemes/palenight.css out.mp4 --sync-with fixtures/schedule-hell.midi --audio fixtures/schedule-hell.flac --grid-size 16x10
+14
colorschemes/palenight.css
··· 1 + :root { 2 + black: #000000; 3 + white: #ffffff; 4 + red: #cf0a2b; 5 + green: #22e753; 6 + blue: #2734e6; 7 + yellow: #f7b337; 8 + orange: #f05811; 9 + purple: #6a24ec; 10 + brown: #a05634; 11 + pink: #e92e76; 12 + gray: #81a0a8; 13 + cyan: #4fecec; 14 + }
+68 -100
src/canvas.rs
··· 1 1 use std::{ 2 2 collections::HashMap, 3 - fs::File, 4 - io::{BufReader, Write}, 3 + io::Write, 5 4 ops::{Range, RangeInclusive}, 6 5 }; 7 6 8 7 use chrono::DateTime; 9 8 use rand::Rng; 10 - use serde::Deserialize; 11 9 12 - use crate::layer::Layer; 10 + use crate::{layer::Layer, Color, ColorMapping}; 13 11 14 - #[derive(Debug, Clone, Default)] 12 + #[derive(Debug, Clone, Default, Copy)] 15 13 pub struct Region { 16 14 pub start: (usize, usize), 17 15 pub end: (usize, usize), 16 + } 17 + 18 + impl std::ops::Sub for Region { 19 + type Output = (i32, i32); 20 + 21 + fn sub(self, rhs: Self) -> Self::Output { 22 + ( 23 + (self.start.0 as i32 - rhs.start.0 as i32), 24 + (self.start.1 as i32 - rhs.start.1 as i32), 25 + ) 26 + } 27 + } 28 + 29 + #[test] 30 + fn test_sub_and_transate_coherence() { 31 + let a = Region::from_origin((3, 3)); 32 + let mut b = a.clone(); 33 + b.translate(2, 3); 34 + 35 + assert_eq!(b - a, (2, 3)); 18 36 } 19 37 20 38 impl Region { ··· 518 536 RawSVG(Box<dyn svg::Node>), 519 537 } 520 538 539 + impl Object { 540 + pub fn translate(&mut self, dx: i32, dy: i32) { 541 + match self { 542 + Object::Polygon(start, lines) => { 543 + start.translate(dx, dy); 544 + for line in lines { 545 + match line { 546 + LineSegment::InwardCurve(anchor) 547 + | LineSegment::OutwardCurve(anchor) 548 + | LineSegment::Straight(anchor) => anchor.translate(dx, dy), 549 + } 550 + } 551 + } 552 + Object::Line(start, end) 553 + | Object::CurveInward(start, end) 554 + | Object::CurveOutward(start, end) 555 + | Object::Rectangle(start, end) => { 556 + start.translate(dx, dy); 557 + end.translate(dx, dy); 558 + } 559 + Object::Text(anchor, _) | Object::Dot(anchor) | Object::SmallCircle(anchor) => { 560 + anchor.translate(dx, dy) 561 + } 562 + Object::BigCircle(center) => center.translate(dx, dy), 563 + Object::RawSVG(_) => { 564 + unimplemented!() 565 + } 566 + } 567 + } 568 + } 569 + 521 570 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 522 571 pub struct Anchor(pub i32, pub i32); 523 572 573 + impl Anchor { 574 + pub fn translate(&mut self, dx: i32, dy: i32) { 575 + self.0 += dx; 576 + self.1 += dy; 577 + } 578 + } 579 + 524 580 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 525 581 pub struct CenterAnchor(pub i32, pub i32); 582 + 583 + impl CenterAnchor { 584 + pub fn translate(&mut self, dx: i32, dy: i32) { 585 + self.0 += dx; 586 + self.1 += dy; 587 + } 588 + } 526 589 527 590 pub trait Coordinates { 528 591 fn coords(&self, cell_size: usize) -> (f32, f32); ··· 577 640 Hatched, 578 641 Dotted, 579 642 } 580 - 581 - #[derive(Debug, Clone, Copy, PartialEq)] 582 - pub enum Color { 583 - Black, 584 - White, 585 - Red, 586 - Green, 587 - Blue, 588 - Yellow, 589 - Orange, 590 - Purple, 591 - Brown, 592 - Cyan, 593 - Pink, 594 - Gray, 595 - } 596 - 597 - impl Default for Color { 598 - fn default() -> Self { 599 - Self::Black 600 - } 601 - } 602 - 603 - #[derive(Debug, Deserialize, Clone)] 604 - pub struct ColorMapping { 605 - pub black: String, 606 - pub white: String, 607 - pub red: String, 608 - pub green: String, 609 - pub blue: String, 610 - pub yellow: String, 611 - pub orange: String, 612 - pub purple: String, 613 - pub brown: String, 614 - pub cyan: String, 615 - pub pink: String, 616 - pub gray: String, 617 - } 618 - 619 - impl ColorMapping { 620 - pub fn default() -> Self { 621 - ColorMapping { 622 - black: "black".to_string(), 623 - white: "white".to_string(), 624 - red: "red".to_string(), 625 - green: "green".to_string(), 626 - blue: "blue".to_string(), 627 - yellow: "yellow".to_string(), 628 - orange: "orange".to_string(), 629 - purple: "purple".to_string(), 630 - brown: "brown".to_string(), 631 - pink: "pink".to_string(), 632 - gray: "gray".to_string(), 633 - cyan: "cyan".to_string(), 634 - } 635 - } 636 - pub fn from_json_file(path: &str) -> ColorMapping { 637 - let file = File::open(path).unwrap(); 638 - let reader = BufReader::new(file); 639 - let json: serde_json::Value = serde_json::from_reader(reader).unwrap(); 640 - ColorMapping { 641 - black: json["black"].as_str().unwrap().to_string(), 642 - white: json["white"].as_str().unwrap().to_string(), 643 - red: json["red"].as_str().unwrap().to_string(), 644 - green: json["green"].as_str().unwrap().to_string(), 645 - blue: json["blue"].as_str().unwrap().to_string(), 646 - yellow: json["yellow"].as_str().unwrap().to_string(), 647 - orange: json["orange"].as_str().unwrap().to_string(), 648 - purple: json["purple"].as_str().unwrap().to_string(), 649 - brown: json["brown"].as_str().unwrap().to_string(), 650 - cyan: json["cyan"].as_str().unwrap().to_string(), 651 - pink: json["pink"].as_str().unwrap().to_string(), 652 - gray: json["gray"].as_str().unwrap().to_string(), 653 - } 654 - } 655 - } 656 - 657 - impl Color { 658 - pub fn to_string(self, mapping: &ColorMapping) -> String { 659 - match self { 660 - Color::Black => mapping.black.to_string(), 661 - Color::White => mapping.white.to_string(), 662 - Color::Red => mapping.red.to_string(), 663 - Color::Green => mapping.green.to_string(), 664 - Color::Blue => mapping.blue.to_string(), 665 - Color::Yellow => mapping.yellow.to_string(), 666 - Color::Orange => mapping.orange.to_string(), 667 - Color::Purple => mapping.purple.to_string(), 668 - Color::Brown => mapping.brown.to_string(), 669 - Color::Cyan => mapping.cyan.to_string(), 670 - Color::Pink => mapping.pink.to_string(), 671 - Color::Gray => mapping.gray.to_string(), 672 - } 673 - } 674 - }
+12 -3
src/cli.rs
··· 5 5 use std::collections::HashMap; 6 6 use std::fs::File; 7 7 use std::io::BufReader; 8 + use std::path::PathBuf; 8 9 9 10 const USAGE: &str = " 10 11 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ··· 136 137 137 138 fn load_colormap(args: &Args) -> ColorMapping { 138 139 if let Some(file) = &args.flag_colors { 139 - let file = File::open(file).unwrap(); 140 - let reader = BufReader::new(file); 141 - serde_json::from_reader(reader).unwrap() 140 + match PathBuf::from(file) 141 + .extension() 142 + .map(|ext| ext.try_into().unwrap()) 143 + { 144 + Some("css") => ColorMapping::from_css_file(file), 145 + Some("json") => ColorMapping::from_json_file(file), 146 + ext => panic!( 147 + "Invalid colormap file format. Must be css or json, is {:?}.", 148 + ext 149 + ), 150 + } 142 151 } else { 143 152 let mut colormap: HashMap<String, String> = HashMap::new(); 144 153 for mapping in &args.flag_color {
+135
src/color.rs
··· 1 + use std::{ 2 + fs::File, 3 + io::{self, BufRead, BufReader}, 4 + }; 5 + 6 + use itertools::Itertools; 7 + use serde::Deserialize; 8 + 9 + #[derive(Debug, Clone, Copy, PartialEq)] 10 + pub enum Color { 11 + Black, 12 + White, 13 + Red, 14 + Green, 15 + Blue, 16 + Yellow, 17 + Orange, 18 + Purple, 19 + Brown, 20 + Cyan, 21 + Pink, 22 + Gray, 23 + } 24 + 25 + impl Default for Color { 26 + fn default() -> Self { 27 + Self::Black 28 + } 29 + } 30 + 31 + #[derive(Debug, Deserialize, Clone)] 32 + pub struct ColorMapping { 33 + pub black: String, 34 + pub white: String, 35 + pub red: String, 36 + pub green: String, 37 + pub blue: String, 38 + pub yellow: String, 39 + pub orange: String, 40 + pub purple: String, 41 + pub brown: String, 42 + pub cyan: String, 43 + pub pink: String, 44 + pub gray: String, 45 + } 46 + 47 + impl ColorMapping { 48 + pub fn default() -> Self { 49 + ColorMapping { 50 + black: "black".to_string(), 51 + white: "white".to_string(), 52 + red: "red".to_string(), 53 + green: "green".to_string(), 54 + blue: "blue".to_string(), 55 + yellow: "yellow".to_string(), 56 + orange: "orange".to_string(), 57 + purple: "purple".to_string(), 58 + brown: "brown".to_string(), 59 + pink: "pink".to_string(), 60 + gray: "gray".to_string(), 61 + cyan: "cyan".to_string(), 62 + } 63 + } 64 + pub fn from_json_file(path: &str) -> ColorMapping { 65 + let file = File::open(path).unwrap(); 66 + let reader = BufReader::new(file); 67 + let json: serde_json::Value = serde_json::from_reader(reader).unwrap(); 68 + ColorMapping { 69 + black: json["black"].as_str().unwrap().to_string(), 70 + white: json["white"].as_str().unwrap().to_string(), 71 + red: json["red"].as_str().unwrap().to_string(), 72 + green: json["green"].as_str().unwrap().to_string(), 73 + blue: json["blue"].as_str().unwrap().to_string(), 74 + yellow: json["yellow"].as_str().unwrap().to_string(), 75 + orange: json["orange"].as_str().unwrap().to_string(), 76 + purple: json["purple"].as_str().unwrap().to_string(), 77 + brown: json["brown"].as_str().unwrap().to_string(), 78 + cyan: json["cyan"].as_str().unwrap().to_string(), 79 + pink: json["pink"].as_str().unwrap().to_string(), 80 + gray: json["gray"].as_str().unwrap().to_string(), 81 + } 82 + } 83 + 84 + pub fn from_css_file(path: &str) -> ColorMapping { 85 + let file = File::open(path).unwrap(); 86 + let lines = std::io::BufReader::new(file).lines(); 87 + let mut mapping = ColorMapping::default(); 88 + for line in lines { 89 + if let Ok(line) = line { 90 + mapping.from_css_line(&line); 91 + } 92 + } 93 + mapping 94 + } 95 + 96 + fn from_css_line(&mut self, line: &str) { 97 + if let Some((name, value)) = line.trim().split_once(":") { 98 + let value = value.trim().to_owned(); 99 + match name.trim() { 100 + "black" => self.black = value, 101 + "white" => self.white = value, 102 + "red" => self.red = value, 103 + "green" => self.green = value, 104 + "blue" => self.blue = value, 105 + "yellow" => self.yellow = value, 106 + "orange" => self.orange = value, 107 + "purple" => self.purple = value, 108 + "brown" => self.brown = value, 109 + "cyan" => self.cyan = value, 110 + "pink" => self.pink = value, 111 + "gray" => self.gray = value, 112 + _ => (), 113 + } 114 + } 115 + } 116 + } 117 + 118 + impl Color { 119 + pub fn to_string(self, mapping: &ColorMapping) -> String { 120 + match self { 121 + Color::Black => mapping.black.to_string(), 122 + Color::White => mapping.white.to_string(), 123 + Color::Red => mapping.red.to_string(), 124 + Color::Green => mapping.green.to_string(), 125 + Color::Blue => mapping.blue.to_string(), 126 + Color::Yellow => mapping.yellow.to_string(), 127 + Color::Orange => mapping.orange.to_string(), 128 + Color::Purple => mapping.purple.to_string(), 129 + Color::Brown => mapping.brown.to_string(), 130 + Color::Cyan => mapping.cyan.to_string(), 131 + Color::Pink => mapping.pink.to_string(), 132 + Color::Gray => mapping.gray.to_string(), 133 + } 134 + } 135 + }
+14 -4
src/layer.rs
··· 1 1 use std::collections::HashMap; 2 2 3 - use crate::canvas::{Color, ColorMapping, Coordinates, Fill, LineSegment, Object, ObjectSizes}; 3 + use crate::{ 4 + canvas::{Coordinates, Fill, LineSegment, Object, ObjectSizes}, 5 + Color, ColorMapping, 6 + }; 4 7 5 8 #[derive(Debug, Clone, Default)] 6 9 pub struct Layer { ··· 27 30 for (_id, (_, maybe_fill)) in &mut self.objects { 28 31 *maybe_fill = Some(fill.clone()); 29 32 } 33 + self._render_cache = None; 34 + } 35 + 36 + pub fn move_all_objects(&mut self, dx: i32, dy: i32) { 37 + self.objects 38 + .iter_mut() 39 + .for_each(|(_, (obj, _))| obj.translate(dx, dy)); 30 40 self._render_cache = None; 31 41 } 32 42 ··· 120 130 path = path.move_to(start.coords(cell_size)); 121 131 for line in lines { 122 132 path = match line { 123 - LineSegment::Straight(end) | LineSegment::InwardCurve(end) | LineSegment::OutwardCurve(end) => { 124 - path.line_to(end.coords(cell_size)) 125 - } 133 + LineSegment::Straight(end) 134 + | LineSegment::InwardCurve(end) 135 + | LineSegment::OutwardCurve(end) => path.line_to(end.coords(cell_size)), 126 136 }; 127 137 } 128 138 path = path.close();
+20 -26
src/lib.rs
··· 1 + mod color; 2 + pub use color::*; 1 3 mod audio; 2 4 pub use audio::*; 3 5 mod sync; ··· 13 15 use indicatif::{ProgressBar, ProgressStyle}; 14 16 pub use midi::MidiSynchronizer; 15 17 use std::cmp::min; 16 - use std::fmt::{Formatter}; 18 + use std::fmt::Formatter; 17 19 use std::fs::{self, create_dir, create_dir_all, remove_dir_all}; 18 20 use std::path::{Path, PathBuf}; 19 21 use std::sync::{Arc, Mutex}; ··· 281 283 .args(["-hide_banner", "-loglevel", "error"]) 282 284 .args(["-framerate", &self.fps.to_string()]) 283 285 // .args(["-pattern_type", "glob"]) // not available on Windows 284 - .args(["-i", &format!("{}/%0{}d.png", self.frames_output_directory, self.total_frames().to_string().len())]) 286 + .args([ 287 + "-i", 288 + &format!( 289 + "{}/%0{}d.png", 290 + self.frames_output_directory, 291 + self.total_frames().to_string().len() 292 + ), 293 + ]) 285 294 .args(["-i", self.audiofile.to_str().unwrap()]) 286 295 .args(["-t", &format!("{}", self.duration_ms() as f32 / 1000.0)]) 287 296 .args(["-vcodec", "png"]) ··· 439 448 let mut hooks = self.hooks; 440 449 hooks.push(Hook { 441 450 when: Box::new(move |_, ctx, _, _| { 442 - for stem_name in stems.split(',').map(|s| s.trim()) { 443 - let stem = ctx.stem(stem_name); 444 - if stem.notes.iter().any(|note| note.is_on()) { 445 - return true; 446 - } 447 - } 448 - false 451 + stems 452 + .split(',') 453 + .map(|n| ctx.stem(n.trim())) 454 + .any(|stem| stem.notes.iter().any(|note| note.is_on())) 449 455 }), 450 456 render_function: Box::new(render_function), 451 457 }); ··· 461 467 let mut hooks = self.hooks; 462 468 hooks.push(Hook { 463 469 when: Box::new(move |_, ctx, _, _| { 464 - for stem_name in stems.split(',') { 465 - let stem = ctx.stem(stem_name); 466 - if stem.notes.iter().any(|note| note.is_off()) { 467 - return true; 468 - } 469 - } 470 - false 470 + stems 471 + .split(',') 472 + .map(|n| ctx.stem(n.trim())) 473 + .any(|stem| stem.notes.iter().any(|note| note.is_off())) 471 474 }), 472 475 render_function: Box::new(render_function), 473 476 }); ··· 614 617 } 615 618 616 619 pub fn render_to(&self, output_file: String, workers_count: usize) -> Result<&Self> { 617 - self.render_composition( 618 - output_file, 619 - self.initial_canvas 620 - .layers 621 - .iter() 622 - .map(|l| l.name.as_str()) 623 - .collect(), 624 - true, 625 - workers_count, 626 - ) 620 + self.render_composition(output_file, vec!["*"], true, workers_count) 627 621 } 628 622 629 623 pub fn render_layers_in(
+10 -34
src/main.rs
··· 27 27 .set_initial_canvas(canvas) 28 28 .init(&|canvas: _, context: _| { 29 29 context.extra = State { 30 - kick_region: Region::from_origin((3, 3)), 31 - background: Color::Black, 30 + kick_region: Region::new(2, 2, 4, 4), 32 31 }; 33 - canvas.set_background(context.extra.background); 32 + canvas.set_background(Color::Black); 34 33 }) 35 34 .set_audio(args.flag_audio.unwrap().into()) 36 35 .sync_audio_with(&args.flag_sync_with.unwrap()) ··· 40 39 canvas.replace_or_create_layer("bass", new_layer); 41 40 }) 42 41 .on_note("anchor kick", &|canvas, ctx| { 43 - ctx.extra.kick_region = region_cycle(&canvas.world_region, &ctx.extra.kick_region) 42 + let new_kick_region = region_cycle(&canvas.world_region, &ctx.extra.kick_region); 43 + 44 + let (dx, dy) = new_kick_region - ctx.extra.kick_region; 45 + canvas.layer("bass").unwrap().move_all_objects(dx, dy); 46 + 47 + ctx.extra.kick_region = new_kick_region; 44 48 }) 45 - .on_note("powerful clap hit, clap", &|canvas, ctx| { 49 + .on_note("powerful clap hit, clap, perclap", &|canvas, ctx| { 46 50 let mut new_layer = canvas.random_layer_within( 47 51 "claps", 48 52 &region_cycle(&canvas.world_region, &ctx.extra.kick_region), ··· 50 54 new_layer.paint_all_objects(Fill::Solid(Color::Red)); 51 55 canvas.replace_or_create_layer("claps", new_layer) 52 56 }) 53 - // .on_stem( 54 - // "bass", 55 - // 0.7, 56 - // &|canvas, context| { 57 - // println!( 58 - // "anchor kick at {}: amplitude_relative is {}", 59 - // context.timestamp, 60 - // context.stem("anchor kick").amplitude_relative() 61 - // ); 62 - // canvas.root().add_object( 63 - // "kick", 64 - // Object::BigCircle(context.extra.1), 65 - // Some(Fill::Solid(Color::Cyan)), 66 - // ); 67 - // }, 68 - // &|canvas, _| canvas.remove_object("kick"), 69 - // ) 70 - .on_stem( 71 - "clap", 72 - 0.7, 73 - &|canvas, _| { 74 - let polygon = canvas.random_polygon(&canvas.world_region); 75 - let fill = Some(Fill::Solid(canvas.random_color())); 76 - canvas.root().add_object("clap", polygon, fill); 77 - }, 78 - &|_, _| {}, 79 - ) 80 57 .on("start credits", &|canvas, _| { 81 58 canvas.root().add_object( 82 59 "credits text", ··· 98 75 #[derive(Default)] 99 76 struct State { 100 77 kick_region: Region, 101 - background: Color, 102 78 } 103 79 104 80 fn color_cycle(current_color: Color) -> Color { ··· 125 101 } 126 102 // Else go to x=0 and move along y axis 127 103 else if current.end.1 + size.1 <= world.end.1 { 128 - new_region = Region::new(0, current.end.1, size.0, current.end.1 + size.1) 104 + new_region = Region::new(2, current.end.1, size.0 + 2, current.end.1 + size.1) 129 105 } 130 106 // Else go to origin 131 107 else {
+11 -11
src/midi.rs
··· 47 47 velocity: note.vel, 48 48 }); 49 49 50 - if is_kick_channel(name) { 51 - // kicks might not have a note off event, so we added one manually after 100ms 52 - notes_per_ms 53 - .entry((note.ms + 100) as usize) 54 - .or_default() 55 - .push(audio::Note { 56 - pitch: note.key, 57 - tick: note.tick, 58 - velocity: 0, 59 - }); 60 - } 50 + // if is_kick_channel(name) { 51 + // // kicks might not have a note off event, so we added one manually after 100ms 52 + // notes_per_ms 53 + // .entry((note.ms + 100) as usize) 54 + // .or_default() 55 + // .push(audio::Note { 56 + // pitch: note.key, 57 + // tick: note.tick, 58 + // velocity: 0, 59 + // }); 60 + // } 61 61 } 62 62 63 63 let duration_ms = notes_per_ms.keys().max().unwrap_or(&0);