mod utils; use noise::*; use rand::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::collections::VecDeque; use std::f64::consts::PI; use std::f64::INFINITY; use svg::node::element::path::Data; use svg::node::element::{Group, Path}; use svg::parser::Event; use svg::Document; use wasm_bindgen::prelude::*; // Input to the art function #[derive(Deserialize)] pub struct Opts { pub hash: String, pub width: f64, pub height: f64, pub pad: f64, } // Feature tells caracteristics of a given art variant // It is returned in the .SVG file #[derive(Clone, Serialize)] pub struct Feature { // which inks are used pub inks: String, // how much inks are used pub inks_count: usize, // which paper is used pub paper: String, // anything special pub special: String, // bold pub bold_each: String, // effect area pub effect_area: String, } #[derive(Clone, Copy, Serialize)] pub struct Ink(&'static str, &'static str, &'static str, f64); #[derive(Clone, Copy, Serialize)] pub struct Paper(&'static str, &'static str, bool); // This is also returned in the SVG to have more metadata for the JS side to render a digital version #[derive(Clone, Serialize)] pub struct Palette { pub primary: Ink, pub secondary: Ink, pub third: Ink, pub paper: Paper, } pub fn art(opts: &Opts, mask_mode: bool) -> (svg::Document, Feature) { let height = opts.height; let width = opts.width; let pad: f64 = opts.pad; let mut rng = rng_from_fxhash(&opts.hash); let mut has_amour = false; let dictionary = if rng.gen_bool(0.02) { has_amour = true; "amour amour amour amour" } else { "independence liberty autonomy determination free will choice flexibility openness diversity tolerance acceptance equality justice fairness creativity progress courage peace joy bliss mindfulness awareness truthfulness authenticity honesty integrity honor respect dignity worthiness gratitude compassion empathy kindness generosity altruism forgiveness harmony unity cooperation collaboration solidarity fraternity" }; let gold_gel = Ink("Gold Gel", "#B92", "#DB4", 0.6); let silver_gel = Ink("Silver Gel", "#CCC", "#FFF", 0.6); let black = Ink("Black", "#111", "#000", 0.35); let brillant_red = Ink("Brillant Red", "#F32", "#931", 0.35); let seibokublue = Ink("Sailor Sei-boku", "#5a82bd", "#348", 0.35); let soft_mint = Ink("Soft Mint", "#00d6b1", "#0a7", 0.35); let turquoise = Ink("Turquoise", "#0AD", "#058", 0.35); let amber = Ink("Amber", "#FB2", "#F80", 0.35); let pink = Ink("Pink", "#ff87a2", "#ee8174", 0.35); let spring_green = Ink("Spring Green", "#783", "#350", 0.35); let white_paper = Paper("White", "#fff", false); let black_paper = Paper("Black", "#111", true); let red_paper = Paper("Red", "#aa0000", true); let perlin: Perlin = Perlin::new(); // PAPER AND INKS let black_paper_chance = 0.1; let red_paper_chance = 0.05; let monochrome_chance = 0.33; let paper = if rng.gen_bool(red_paper_chance) { red_paper } else if rng.gen_bool(black_paper_chance) { black_paper } else { white_paper }; let count = if paper.2 { rng.gen_range(1, 3) } else { rng.gen_range(1, 4) }; let mut words = dictionary.split(" ").collect::>(); rng.shuffle(&mut words); words = words[0..count].to_vec(); let mut colors = if paper.2 { vec![silver_gel, gold_gel] } else { vec![ black, seibokublue, amber, soft_mint, pink, turquoise, brillant_red, spring_green, ] }; rng.shuffle(&mut colors); let monochrome = rng.gen_bool(monochrome_chance); let ink_count = if monochrome { 2 } else { count }; colors = colors[0..ink_count].to_vec(); let third_to_first = ink_count == 3 && rng.gen_bool(0.5); if third_to_first { colors[2] = colors[0]; } // TEXT STYLES let font_size = rng.gen_range(22.0, 32.0) * colors[0].3; let word_dist = (0.04 + rng.gen_range(-0.1, 0.6) * rng.gen_range(0.0, 1.0)) * font_size; let line_dist = (0.3 + 0.3 * rng.gen_range(0f64, 1.0).powi(3) - 0.15 * rng.gen_range(0f64, 1.0).powi(5)) * font_size; // VALUE FUNCTIONS let color_anomaly = 0.0008; let repeat_divisor = (1.0 + rng.gen_range(0.0, 4.0) * rng.gen_range(0.0, 1.0) * rng.gen_range(0.0, 1.0)) as usize; let color_word_attached = rng.gen_bool(0.2); let color_seed = rng.gen_range(0.0, 100000.0); let color_freq = 0.5 + rng.gen_range(0.0, 16.0) * rng.gen_range(0.0, 1.0); let color_field = rng.gen_bool(0.7); let vsplit = rng.gen_bool(0.3); let hsplit = rng.gen_bool(0.3); let mut has_concentric = false; let mut concentric_color_add = vec![]; if rng.gen_bool(0.4) { concentric_color_add.push((0.0, rng.gen_range(0.0, 0.2))); has_concentric = true; } if rng.gen_bool(0.3) { concentric_color_add .push((rng.gen_range(0.2, 0.3), rng.gen_range(0.3, 0.4))); has_concentric = true; } if rng.gen_bool(0.4) { concentric_color_add.push((rng.gen_range(0.35, 0.45), 0.5)); has_concentric = true; } let clr_mod = ink_count + if rng.gen_bool(0.1) { 1 } else { 0 }; let color_fn = |rng: &mut StdRng, pos: (f64, f64), i: usize| -> usize { if rng.gen_bool(color_anomaly) { return rng.gen_range(0, clr_mod); } if monochrome { return 0; } let mut color = 0; if color_word_attached { color = (i / repeat_divisor) % clr_mod; } else if color_field { let v = perlin.get([ color_seed + pos.0 * color_freq / width, color_seed + pos.1 * color_freq / width, ]); let v = (v + 0.5) * (clr_mod as f64); color = v.floor() as usize % clr_mod; } if concentric_color_add.len() > 0 { let dist_center = ((pos.0 - width / 2.0).powi(2) + (pos.1 - height / 2.0).powi(2)).sqrt() / width; for &(from, to) in concentric_color_add.iter() { if dist_center > from && dist_center < to { color += 1; } } } if vsplit { if pos.0 < width / 2.0 { color += 1; } } if hsplit { if pos.1 < height / 2.0 { color += 1; } } color % clr_mod }; let mut concentric_rays_bold = vec![]; if rng.gen_bool(0.1) { concentric_rays_bold.push((0.43, 0.5)); } let bold_activate = !paper.2 && rng.gen_bool(0.2); let bold_mod = rng.gen_range(3, 7); let bold_fn = |_rng: &mut StdRng, _pos: (f64, f64), i: usize| -> bool { return bold_activate && (i % bold_mod == 0); }; let mut routes = Vec::new(); let svg_content = r###" "###; // add text let non_attached_pad = 0.0; let extra_pad = 1.0; let letters_ref: LetterSvgReferential = LetterSvgReferential::new(svg_content, 0.1, non_attached_pad, extra_pad); let mut unsafe_curves = Vec::new(); let mut spiral = spiral_optimized( width / 2.0, height / 2.0, width / 2.0 - pad, line_dist, 0.1, ); let has_thread = if rng.gen_bool(0.02) { spiral = vec![vec![(width - pad, pad)], spiral].concat(); true } else { false }; unsafe_curves.push(spiral); let curves = unsafe_curves.clone(); // offset text exactly on the curve line let yoffset = -font_size * 0.5; let mut queue = VecDeque::new(); for word in words { queue.push_back(word.to_string()); } let mut total_words = 0; let mut offsets = vec![]; let b = 0.01 * font_size; for i in 0..3 { let angle = i as f64 * 2.0 * PI / 3.0; let offset = (angle.cos() * b, angle.sin() * b); offsets.push(offset); } for c in curves.clone() { let length = curve_length(&c); let extrapad = word_dist; let mut subset = vec![]; let mut sum = extrapad; let mut sum_words = 0.0; // we try to pull as much word as we can to fill the curve while let Some(word) = queue.pop_front() { let text = word.clone(); let measure = measure_text(&letters_ref, text.clone(), font_size); if sum + measure + word_dist < length { sum += measure + word_dist; sum_words += measure; subset.push(text); queue.push_back(word.clone()); } else { queue.push_front(word.clone()); break; } } if subset.len() == 0 { continue; } // we will equilibrate the padding for all the words to smoothly occupy the curve let pad = (length - sum_words) / (subset.len() as f64); let mut xstart = 0.0; for text in subset { xstart += pad / 2.0; let res = draw_text(&letters_ref, text.clone(), font_size, xstart, yoffset, &c); if res.0.len() == 0 { continue; } let pos = calc_text_center(&res.0); let clr_index = color_fn(&mut rng, pos, total_words); let bold = bold_fn(&mut rng, pos, total_words); let rts = res.0; if bold { for offset in offsets.iter() { routes.extend( rts .iter() .map(|r| { ( clr_index, r.iter().map(|p| (p.0 + offset.0, p.1 + offset.1)).collect(), ) }) .collect::>(), ); } } else { routes.extend( rts .iter() .map(|r| (clr_index, r.clone())) .collect::>(), ); } total_words += 1; xstart += res.1 + pad / 2.0; } } let mut has_empty_word = false; let colors_count = colors.len(); let mut color_presence = vec![false; colors_count]; for (i, _) in routes.iter() { if *i < colors_count { color_presence[*i] = true; } else { has_empty_word = true; } } let mut inks = vec![]; for (i, &present) in color_presence.iter().enumerate() { if present && !inks.contains(&colors[i].0) { inks.push(colors[i].0); } } inks.sort(); let inks_length = inks.len(); let mut specials = vec![]; if has_thread { specials.push("Thread"); } if has_amour { specials.push("Amour"); } if has_empty_word { specials.push("Empty Colors"); } let mut area_effects = vec![]; if inks_length > 1 { if vsplit { area_effects.push("V-Split"); } if hsplit { area_effects.push("H-Split"); } if has_concentric { area_effects.push("Concentric"); } if color_field { area_effects.push("Color Field"); } } let feature = Feature { inks: inks.join(", "), inks_count: inks_length, paper: paper.0.to_string(), special: specials.join(", "), bold_each: if bold_activate { format!("{}", bold_mod) } else { "".to_string() }, effect_area: area_effects.join(", "), }; let feature_json = serde_json::to_string(&feature).unwrap(); let palette_json = serde_json::to_string(&Palette { paper, primary: colors[0 % colors.len()], secondary: colors[1 % colors.len()], third: colors[2 % colors.len()], }) .unwrap(); let mask_colors = vec!["#0FF", "#F0F", "#FF0"]; let layers = make_layers( colors .iter() .enumerate() .map(|(i, c)| { ( if mask_mode { mask_colors[i] } else { c.1 }, c.0.to_string(), c.3, routes .iter() .filter_map( |(ci, routes)| { if *ci == i { Some(routes.clone()) } else { None } }, ) .collect(), ) }) .collect(), ); let mut document = svg::Document::new() .set( "data-credits", "@greweb - 2023 - GREWEBARTHURCOLLAB".to_string(), ) .set("data-hash", opts.hash.to_string()) .set("data-traits", feature_json) .set("data-palette", palette_json) .set("viewBox", (0, 0, width, height)) .set("width", format!("{}mm", width)) .set("height", format!("{}mm", height)) .set( "style", if mask_mode { "background:white".to_string() } else { format!("background:{}", paper.1) }, ) .set( "xmlns:inkscape", "http://www.inkscape.org/namespaces/inkscape", ) .set("xmlns", "http://www.w3.org/2000/svg"); for l in layers { document = document.add(l); } (document, feature) } #[wasm_bindgen] pub fn render(val: &JsValue) -> String { let opts = val.into_serde().unwrap(); let (doc, _) = art(&opts, true); let str = doc.to_string(); return str; } fn render_route(data: Data, route: Vec<(f64, f64)>) -> Data { if route.len() == 0 { return data; } let first_p = route[0]; let mut d = data.move_to((significant_mm(first_p.0), significant_mm(first_p.1))); for p in route { d = d.line_to((significant_mm(p.0), significant_mm(p.1))); } return d; } #[inline] fn significant_mm(f: f64) -> f64 { (f * 100.0).floor() / 100.0 } fn make_layers( data: Vec<(&str, String, f64, Vec>)>, ) -> Vec { let layers: Vec = data .iter() .filter(|(_color, _label, _stroke_width, routes)| routes.len() > 0) .enumerate() .map(|(ci, (color, label, stroke_width, routes))| { let mut l = Group::new() .set("inkscape:groupmode", "layer") .set("inkscape:label", format!("{} {}", ci, label.clone())) .set("fill", "none") .set("stroke", color.clone()) .set("stroke-linecap", "round") .set("stroke-width", *stroke_width); let opacity: f64 = 0.7; let opdiff = 0.15 / (routes.len() as f64); let mut trace = 0f64; for route in routes.clone() { trace += 1f64; let data = render_route(Data::new(), route); l = l.add( Path::new() .set( "opacity", (1000. * (opacity - trace * opdiff)).floor() / 1000.0, ) .set("d", data), ); } l }) .collect(); layers } #[inline] fn euclidian_dist((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 { let dx = x1 - x2; let dy = y1 - y2; return (dx * dx + dy * dy).sqrt(); } fn draw_text( letter_ref: &LetterSvgReferential, text: String, // text to draw size: f64, // font size xstart: f64, // x move on the path yoffset: f64, // make diff baseline path: &Vec<(f64, f64)>, // curve to follow ) -> (Vec>, f64) { let mut routes = Vec::new(); let mut x = 0.; let mut y = 0.; let mut prev_can_attach = false; let mut last: Vec<(f64, f64)> = vec![]; for c in text.chars() { if let Some(letter) = letter_ref.get_letter(&c.to_string()) { let (rts, (dx, dy)) = letter.render((x, y), size, false); if prev_can_attach && letter.can_attach_left { let mut rts = rts.clone(); let mut add = rts.pop().unwrap(); // interpolate curve to attach more smoothly if last.len() > 0 { let lastp = last[last.len() - 1]; let firstp = add[0]; // ygap between last and first let ygap = firstp.1 - lastp.1; let mut i = 1; let mut maxlen = 0.5 * size; while i < add.len() { if maxlen < 0. { break; } let l = euclidian_dist(add[i - 1], add[i]); if ygap > 0.0 { if add[i].1 < lastp.1 { break; } } else { if add[i].1 > lastp.1 { break; } } i += 1; maxlen -= l; } if i == add.len() { i -= 1; } let stopi = i; add = add .iter() .enumerate() .map(|(i, &p)| { if i <= stopi { let y = p.1 - ygap * (1.0 - i as f64 / stopi as f64); (p.0, y) } else { p } }) .collect(); } last.extend(add); routes.extend(rts); // ° on i and j } else { if last.len() > 0 { routes.push(last); last = vec![]; } routes.extend(rts); } prev_can_attach = letter.can_attach_right; x += dx; y += dy; } else { prev_can_attach = false; // println!("letter not found: {}", c); } } if last.len() > 0 { routes.push(last); } // rotate with angle and translate to origin all routes let mut proj_routes = Vec::new(); for route in routes { let mut proj_route = Vec::new(); for (x, y) in route { // use x to find position in path and project x,y let (origin, a) = lookup_curve_point_and_angle(&path, x + xstart); let y = y + yoffset; let disp = (-y * a.sin(), y * a.cos()); let p = (origin.0 + disp.0, origin.1 + disp.1); proj_route.push(p); } proj_routes.push(proj_route); } (proj_routes, x) } fn angle2(p1: (f64, f64), p2: (f64, f64)) -> f64 { let (x1, y1) = p1; let (x2, y2) = p2; let dx = x2 - x1; let dy = y2 - y1; dy.atan2(dx) } fn curve_length(path: &Vec<(f64, f64)>) -> f64 { let mut len = 0.0; for i in 0..path.len() - 1 { len += euclidian_dist(path[i], path[i + 1]); } len } fn measure_text( letter_ref: &LetterSvgReferential, text: String, size: f64, ) -> f64 { let mut x = 0.; for c in text.chars() { if let Some(letter) = letter_ref.get_letter(&c.to_string()) { let (dx, _dy) = letter.render((x, 0.0), size, false).1; x += dx; } } x } fn lookup_curve_point_and_angle( path: &Vec<(f64, f64)>, l: f64, ) -> ((f64, f64), f64) { let mut i = 0; if l < 0.0 { return (path[0], angle2(path[0], path[1])); } let mut len = 0.0; while i < path.len() - 1 { let l1 = euclidian_dist(path[i], path[i + 1]); if len + l1 > l { let r = (l - len) / l1; let x = path[i].0 + r * (path[i + 1].0 - path[i].0); let y = path[i].1 + r * (path[i + 1].1 - path[i].1); let angle = angle2(path[i], path[i + 1]); return ((x, y), angle); } len += l1; i += 1; } return ( path[path.len() - 1], angle2(path[path.len() - 2], path[path.len() - 1]), ); } #[derive(Clone)] struct Letter { pub routes: Vec>, pub width: f64, pub height: f64, pub can_attach_left: bool, pub can_attach_right: bool, } impl Letter { fn new( routes: Vec>, width: f64, height: f64, can_attach_left: bool, can_attach_right: bool, ) -> Letter { Letter { routes, width, height, can_attach_left, can_attach_right, } } fn render( &self, (x, y): (f64, f64), size: f64, vertical: bool, ) -> (Vec>, (f64, f64)) { let mut routes = self.routes.clone(); let w = self.width; let h = self.height; let ratio = w / h; let scale = size / h; for route in routes.iter_mut() { for p in route.iter_mut() { p.0 *= scale; p.1 *= scale; if vertical { *p = (h * scale - p.1, p.0); } p.0 += x; p.1 += y; } } let delta = if vertical { (0.0, ratio * size) } else { (ratio * size, 0.0) }; (routes, delta) } } #[derive(Clone)] struct LetterSvgReferential { letters: HashMap, } impl LetterSvgReferential { fn new( content: &str, letter_precision: f64, non_attached_pad: f64, extra_pad: f64, ) -> LetterSvgReferential { let mut height = 0.0; let mut documents_per_layer: HashMap = HashMap::new(); for event in svg::read(content).unwrap() { match event { Event::Tag(_, _, attributes) => { if let Some(c) = attributes.get("inkscape:label") { if let Some(d) = attributes.get("d") { let data: String = d.to_string(); let document = Document::new().add(Path::new().set("d", data)).to_string(); documents_per_layer.insert(c.to_string(), document); } } if let Some(h) = attributes.get("height") { let mut hv = h.to_string(); hv = hv.replace("mm", ""); if let Some(h) = hv.parse::().ok() { height = h; } } } _ => {} } } let mut letters = HashMap::new(); for (c, svg) in documents_per_layer.iter() { let polylines = svg2polylines::parse(svg.as_str(), letter_precision, true).unwrap(); let mut minx = std::f64::INFINITY; let mut maxx = -std::f64::INFINITY; for poly in polylines.iter() { for p in poly.iter() { if p.x < minx { minx = p.x; } if p.x > maxx { maxx = p.x; } } } let mut width = maxx - minx; let mut dx = minx; let letter_name = c[0..1].to_string(); // < : can attach left let can_attach_left = c.contains("<"); // > : can attach right let can_attach_right = c.contains(">"); // R : add extra pad on the right let add_extra_pad_right = c.contains("R"); if !can_attach_left { dx -= non_attached_pad; width += non_attached_pad; } if !can_attach_right { width += non_attached_pad; } if add_extra_pad_right { width += extra_pad; } /* if !can_attach { dx -= non_attached_pad; width += 2.0 * non_attached_pad; } */ let routes: Vec> = polylines .iter() .map(|l| l.iter().map(|p| (p.x - dx, p.y)).collect()) .collect(); letters.insert( letter_name.clone(), Letter::new(routes, width, height, can_attach_left, can_attach_right), ); } letters.insert( " ".to_string(), Letter::new(vec![], 0.5 * height, height, false, false), ); LetterSvgReferential { letters } } fn get_letter(&self, c: &String) -> Option<&Letter> { self.letters.get(c) } } fn spiral_optimized( x: f64, y: f64, radius: f64, dr: f64, approx: f64, ) -> Vec<(f64, f64)> { let two_pi = 2.0 * PI; let mut route = Vec::new(); let mut r = radius; let mut a = 0f64; loop { let p = ( significant_mm(x + r * a.cos()), significant_mm(y + r * a.sin()), ); let l = route.len(); if l == 0 || euclidian_dist(route[l - 1], p) > approx { route.push(p); } let da = 1.0 / (r + 8.0); // bigger radius is more we have to do angle iterations a = (a + da) % two_pi; r -= dr * da / two_pi; if r < 0.05 { break; } } route } fn calc_text_center(routes: &Vec>) -> (f64, f64) { let mut min_x = INFINITY; let mut max_x = -INFINITY; let mut min_y = INFINITY; let mut max_y = -INFINITY; for route in routes.iter() { for p in route.iter() { if p.0 < min_x { min_x = p.0; } if p.0 > max_x { max_x = p.0; } if p.1 < min_y { min_y = p.1; } if p.1 > max_y { max_y = p.1; } } } ((min_x + max_x) / 2.0, (min_y + max_y) / 2.0) } fn rng_from_fxhash(hash: &String) -> StdRng { let mut bs = [0; 32]; bs58::decode(hash.chars().skip(2).take(43).collect::()) .into(&mut bs) .unwrap(); let rng = StdRng::from_seed(bs); return rng; }