shapemaker 1.1.1

An experimental WASM-capable, generative SVG-based video rendering engine that reacts to MIDI or audio data
Documentation
pub mod ui;

use crate::{Canvas, ColorMapping};
use docopt::Docopt;
use measure_time::info_time;
use serde::Deserialize;

const USAGE: &str = "
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
█░▄▄█░████░▄▄▀█▀▄▄▀█░▄▄█░▄▀▄░█░▄▄▀█░█▀█░▄▄█░▄▄▀█
█▄▄▀█░▄▄░█░▀▀░█░▀▀░█░▄▄█░█▄█░█░▀▀░█░▄▀█░▄▄█░▀▀▄█
█▄▄▄█▄██▄█▄██▄█░████▄▄▄█▄███▄█▄██▄█▄█▄█▄▄▄█▄█▄▄█
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀v?.?.?▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

Usage: shapemaker (image|video) [options] [--color <mapping>...] <file>
       shapemaker --help
       shapemaker --version

Options:
    --resolution <pixelcount>      Size of the image (or frames)'s largest dimension in pixels [default: 1000]
    --colors <file>                JSON file mapping color names to hex values
                                   The supported color names are: black, white, red, green, blue, yellow, orange, purple, brown, pink, gray, and cyan.
    -c --color <mapping>           Color mapping in the form of <color>:<hex>. Can be used multiple times.
    --grid-size <WIDTHxHEIGHT>     Size of the grid (number of anchor points) [default: 3x3]
                                   Putting one of the dimensions to 1 can cause a crash.
    --cell-size <size>             Size of a cell in pixels [default: 50]
    --canvas-padding <size>        Outter canvas padding between cells in pixels [default: 10]
    --line-width <size>            Width of the lines in pixels [default: 2]
    --small-circle-radius <size>   Radius of small circles in pixels [default: 5]
    --dot-radius <size>            Radius of dots in pixels [default: 2]
    --empty-shape-stroke <size>    Width of the stroke when a closed shape is not filled [default: 0.5]
    --render-grid                  Render the grid of anchor points
    --objects-count <range>        Number of objects to render [default: 3..6]
    --polygon-vertices <range>     Number of vertices for polygons [default: 2..6]

        Note: <range>s are inclusive on both ends

    Video-specific:
    --workers <number>             Number of parallel threads to use for rendering [default: 8]
    --fps <fps>                    Frames per second [default: 30]
    --audio <file>                 Audio file to use for the video
    --duration <seconds>           Number of seconds to render. If not set, the video will be as long as the audio file.
    --start <seconds>              Start the video at this time in seconds. [default: 0]
    --sync-with <directory>        Directory containing the audio files to sync to.
                                   The directory must contain:
                                   - stems/(instrument name).wav — stems
                                   - landmarks.json — JSON file mapping time in milliseconds to marker text (see ./landmarks.py)
                                   - full.mp3 — the complete audio file to use as the video's audio
                                   - bpm.txt — the BPM of the audio file (see ./landmarks.py)


";

pub fn cli_args() -> Args {
    let args: Args = Docopt::new(USAGE.replace("?.?.?", env!("CARGO_PKG_VERSION")))
        .and_then(|d| d.deserialize())
        .unwrap_or_else(|e| e.exit());

    if args.flag_version {
        println!("shapemaker {}", env!("CARGO_PKG_VERSION"));
        std::process::exit(0);
    }

    args
}

pub fn canvas_from_cli(args: &Args) -> Canvas {
    info_time!("canvas_from_cli");
    let mut canvas = Canvas::new(vec![]);
    canvas.colormap = load_colormap(args);
    set_canvas_settings_from_args(args, &mut canvas);
    canvas
}

#[derive(Debug, Deserialize)]
pub struct Args {
    pub cmd_image: bool,
    pub cmd_video: bool,
    pub arg_file: String,
    pub flag_version: bool,
    pub flag_color: Vec<String>,
    pub flag_colors: Option<String>,
    pub flag_grid_size: Option<String>,
    pub flag_cell_size: Option<usize>,
    pub flag_canvas_padding: Option<usize>,
    pub flag_line_width: Option<f32>,
    pub flag_small_circle_radius: Option<f32>,
    pub flag_dot_radius: Option<f32>,
    pub flag_empty_shape_stroke: Option<f32>,
    pub flag_render_grid: bool,
    pub flag_objects_count: Option<String>,
    pub flag_polygon_vertices: Option<String>,
    pub flag_fps: Option<usize>,
    pub flag_sync_with: Option<String>,
    pub flag_audio: Option<String>,
    pub flag_resolution: Option<u32>,
    pub flag_workers: Option<usize>,
    pub flag_duration: Option<usize>,
    pub flag_start: Option<usize>,
}

fn set_canvas_settings_from_args(args: &Args, canvas: &mut Canvas) {
    if let Some(dimensions) = &args.flag_grid_size {
        let mut split = dimensions.split('x');
        let width = split.next().unwrap().parse::<usize>().unwrap();
        let height = split.next().unwrap().parse::<usize>().unwrap();
        canvas.set_grid_size(width, height);
    }
    if let Some(cell_size) = args.flag_cell_size {
        canvas.cell_size = cell_size;
    }
    if let Some(canvas_padding) = args.flag_canvas_padding {
        canvas.canvas_outter_padding = canvas_padding;
    }
    if let Some(line_width) = args.flag_line_width {
        canvas.object_sizes.default_line_width = line_width;
    }
    if let Some(small_circle_radius) = args.flag_small_circle_radius {
        canvas.object_sizes.small_circle_radius = small_circle_radius;
    }
    if let Some(dot_radius) = args.flag_dot_radius {
        canvas.object_sizes.dot_radius = dot_radius;
    }
    if let Some(empty_shape_stroke) = args.flag_empty_shape_stroke {
        canvas.object_sizes.empty_shape_stroke_width = empty_shape_stroke;
    }
    if let Some(objects_count) = &args.flag_objects_count {
        let mut split = objects_count.split("..");
        let min = split.next().unwrap().parse::<usize>().unwrap();
        let max = split.next().unwrap().parse::<usize>().unwrap();
        // +1 because the range is exclusive, using ..= raises a type error
        canvas.objects_count_range = min..(max + 1);
    }
    if let Some(polygon_vertices) = &args.flag_polygon_vertices {
        let mut split = polygon_vertices.split("..");
        let min = split.next().unwrap().parse::<usize>().unwrap();
        let max = split.next().unwrap().parse::<usize>().unwrap();
        canvas.polygon_vertices_range = min..(max + 1);
    }
}

fn load_colormap(args: &Args) -> ColorMapping {
    if let Some(file) = &args.flag_colors {
        ColorMapping::from_file(file.into())
    } else {
        ColorMapping::from_cli_args(&args.flag_color)
    }
}