├── .gitignore ├── src ├── .gitattributes ├── lib.rs ├── art │ └── colors_used.rs ├── bin │ └── qql-cli.rs ├── sectors.rs ├── traits.rs ├── color.rs ├── config.rs ├── layouts.rs ├── math.rs ├── rand.rs └── colordata.json ├── docs └── img │ ├── anderszoom.png │ ├── qql085_face.png │ ├── qql053_groups.gif │ ├── qql237_how_to_viewport.png │ ├── marketplace_negspace_example.png │ ├── qql024_epicenter_minsteps_64.png │ ├── qql024_epicenter_minsteps_8.png │ └── qql024_epicenter_minsteps_diff.png ├── goldens ├── 0x33c9371d25ce44a408f8a6473fbad86bf81e1a17a2e52c90cf66ffff1296712e.png ├── 0x4c61496e282ba45975b6863f14aeed35d686abfe78273b39ee44ffff146a6246.png └── 0xb788f929c27e0a6e9abfc2a66ad878d73a930d128e1b0f08e009ffff10d10d4b.png ├── Cargo.toml ├── LICENSE-MIT ├── tests └── golden.rs ├── LICENSE-APACHE ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/.gitattributes: -------------------------------------------------------------------------------- 1 | colordata.json binary 2 | -------------------------------------------------------------------------------- /docs/img/anderszoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/docs/img/anderszoom.png -------------------------------------------------------------------------------- /docs/img/qql085_face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/docs/img/qql085_face.png -------------------------------------------------------------------------------- /docs/img/qql053_groups.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/docs/img/qql053_groups.gif -------------------------------------------------------------------------------- /docs/img/qql237_how_to_viewport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/docs/img/qql237_how_to_viewport.png -------------------------------------------------------------------------------- /docs/img/marketplace_negspace_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/docs/img/marketplace_negspace_example.png -------------------------------------------------------------------------------- /docs/img/qql024_epicenter_minsteps_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/docs/img/qql024_epicenter_minsteps_64.png -------------------------------------------------------------------------------- /docs/img/qql024_epicenter_minsteps_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/docs/img/qql024_epicenter_minsteps_8.png -------------------------------------------------------------------------------- /docs/img/qql024_epicenter_minsteps_diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/docs/img/qql024_epicenter_minsteps_diff.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod art; 2 | pub mod color; 3 | pub mod config; 4 | pub mod layouts; 5 | pub mod math; 6 | pub mod rand; 7 | pub mod sectors; 8 | pub mod traits; 9 | 10 | pub fn unit() {} 11 | -------------------------------------------------------------------------------- /goldens/0x33c9371d25ce44a408f8a6473fbad86bf81e1a17a2e52c90cf66ffff1296712e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/goldens/0x33c9371d25ce44a408f8a6473fbad86bf81e1a17a2e52c90cf66ffff1296712e.png -------------------------------------------------------------------------------- /goldens/0x4c61496e282ba45975b6863f14aeed35d686abfe78273b39ee44ffff146a6246.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/goldens/0x4c61496e282ba45975b6863f14aeed35d686abfe78273b39ee44ffff146a6246.png -------------------------------------------------------------------------------- /goldens/0xb788f929c27e0a6e9abfc2a66ad878d73a930d128e1b0f08e009ffff10d10d4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qql-art/qqlrs/HEAD/goldens/0xb788f929c27e0a6e9abfc2a66ad878d73a930d128e1b0f08e009ffff10d10d4b.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qql" 3 | version = "0.1.0" 4 | edition = "2021" 5 | default-run = "qql-cli" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | name = "qql" 11 | 12 | [[bin]] 13 | name = "qql-cli" 14 | 15 | [profile.release] 16 | debug = true 17 | 18 | [dependencies] 19 | anyhow = "1.0.70" 20 | clap = { version = "4.2.4", features = ["derive"] } 21 | hex = "0.4.3" 22 | hex-literal = "0.3.4" 23 | raqote = "0.8.2" 24 | serde = { version = "1.0.160", features = ["derive"] } 25 | serde_json = "1.0.96" 26 | 27 | [dev-dependencies] 28 | hex-literal = "0.3.4" 29 | image = { version = "0.24.7", default-features = false, features = ["png"] } 30 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/art/colors_used.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::color::ColorKey; 4 | 5 | #[derive(Default, PartialEq)] 6 | pub struct ColorsUsed { 7 | vector: Vec, 8 | set: HashSet, 9 | } 10 | 11 | impl std::fmt::Debug for ColorsUsed { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | self.vector.fmt(f) 14 | } 15 | } 16 | 17 | impl ColorsUsed { 18 | pub fn new() -> Self { 19 | Self::default() 20 | } 21 | 22 | pub fn insert(&mut self, color: ColorKey) { 23 | if self.set.insert(color) { 24 | self.vector.push(color); 25 | } 26 | } 27 | 28 | pub fn as_slice(&self) -> &[ColorKey] { 29 | self.vector.as_slice() 30 | } 31 | 32 | pub fn iter(&self) -> <&'_ Self as IntoIterator>::IntoIter { 33 | self.into_iter() 34 | } 35 | } 36 | 37 | impl Extend for ColorsUsed { 38 | fn extend>(&mut self, iter: T) { 39 | for color in iter { 40 | self.insert(color); 41 | } 42 | } 43 | } 44 | 45 | impl<'a> IntoIterator for &'a ColorsUsed { 46 | type Item = ColorKey; 47 | type IntoIter = std::iter::Copied>; 48 | fn into_iter(self) -> Self::IntoIter { 49 | self.vector.iter().copied() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/golden.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::BufReader; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use anyhow::Context; 6 | use hex_literal::hex; 7 | use image::ImageFormat; 8 | use raqote::DrawTarget; 9 | 10 | // Set this environment variable to any non-empty string to write golden files (trivially passing 11 | // the test) instead of checking them. 12 | const ENV_UPDATE_GOLDENS: &str = "QQLRS_UPDATE_GOLDENS"; 13 | 14 | const GOLDEN_WIDTH: i32 = 800; 15 | 16 | fn test_golden(seed: [u8; 32]) -> anyhow::Result<()> { 17 | let golden_filepath = PathBuf::from_iter([ 18 | env!("CARGO_MANIFEST_DIR"), 19 | "goldens", 20 | format!("0x{}.png", hex::encode(seed)).as_str(), 21 | ]); 22 | 23 | let color_db = qql::color::ColorDb::from_bundle(); 24 | let mut config = qql::config::Config::default(); 25 | config.chunks = "2x2".parse().unwrap(); 26 | 27 | let canvas = qql::art::draw(&seed, &color_db, &config, GOLDEN_WIDTH, |_| {}).canvas; 28 | if std::env::var_os(ENV_UPDATE_GOLDENS).is_some_and(|v| !v.is_empty()) { 29 | write_golden(&canvas, golden_filepath.as_ref()) 30 | } else { 31 | check_golden(&canvas, golden_filepath.as_ref()) 32 | } 33 | } 34 | 35 | fn write_golden(dt: &DrawTarget, golden_filepath: &Path) -> anyhow::Result<()> { 36 | dt.write_png(&golden_filepath) 37 | .context("Failed to write golden PNG") 38 | } 39 | 40 | fn check_golden(dt: &DrawTarget, golden_filepath: &Path) -> anyhow::Result<()> { 41 | let reader = BufReader::new( 42 | File::open(golden_filepath) 43 | .with_context(|| format!("Failed to read golden at {}", golden_filepath.display()))?, 44 | ); 45 | let reader = image::io::Reader::with_format(reader, ImageFormat::Png); 46 | let golden = reader 47 | .decode() 48 | .context("Failed to decode image")? 49 | .into_rgba8(); 50 | 51 | assert_eq!( 52 | (dt.width() as u32, dt.height() as u32), 53 | (golden.width(), golden.height()) 54 | ); 55 | 56 | let actual_pixels = dt.get_data().iter(); 57 | let golden_pixels = golden.enumerate_pixels(); 58 | for (actual_px, (x, y, golden_px)) in actual_pixels.zip(golden_pixels) { 59 | let [ab, ag, ar, _aa] = actual_px.to_le_bytes(); 60 | let [gr, gg, gb, _ga] = golden_px.0; 61 | // Use a simple L-infinity norm for now. Can refine if we need to. 62 | assert_px_close((x, y), (ar, ag, ab), (gr, gg, gb)); 63 | } 64 | 65 | Ok(()) 66 | } 67 | 68 | fn assert_px_close((x, y): (u32, u32), actual: (u8, u8, u8), golden: (u8, u8, u8)) { 69 | const THRESHOLD: u32 = 16; 70 | let dr = channel_delta(actual.0, golden.0); 71 | let dg = channel_delta(actual.1, golden.1); 72 | let db = channel_delta(actual.2, golden.2); 73 | if dr > THRESHOLD || dg > THRESHOLD || db > THRESHOLD { 74 | panic!( 75 | "at ({}, {}): expected ~{:?}, got {:?}; max allowed deviation is {}", 76 | x, y, golden, actual, THRESHOLD 77 | ); 78 | } 79 | } 80 | 81 | fn channel_delta(u: u8, v: u8) -> u32 { 82 | ((u as i32) - (v as i32)).abs() as u32 83 | } 84 | 85 | // Pick some QQLs to cover all the `Structure::*` and `ColorMode::*` options, as well as the linear 86 | // and radial variants of `FlowField::*`, since those have the most kinds of distinct codepaths. 87 | // Within those constraints, select for QQLs that render quickly and generate small output files. 88 | 89 | /// QQL #77 uses `Structure::Formation`, `FlowField::RandomLinear`, and `ColorMode::Zebra`. 90 | #[test] 91 | fn golden_qql077() -> anyhow::Result<()> { 92 | let seed = hex!("b788f929c27e0a6e9abfc2a66ad878d73a930d128e1b0f08e009ffff10d10d4b"); 93 | test_golden(seed) 94 | } 95 | 96 | /// QQL #219 uses `Structure::Shadows`, `FlowField::Circular`, and `ColorMode::Simple`. 97 | #[test] 98 | fn golden_qql219() -> anyhow::Result<()> { 99 | let seed = hex!("33c9371d25ce44a408f8a6473fbad86bf81e1a17a2e52c90cf66ffff1296712e"); 100 | test_golden(seed) 101 | } 102 | 103 | /// QQL #234 uses `Structure::Orbital`, `FlowField::Circular`, and `ColorMode::Stacked`. 104 | #[test] 105 | fn golden_qql234() -> anyhow::Result<()> { 106 | let seed = hex!("4c61496e282ba45975b6863f14aeed35d686abfe78273b39ee44ffff146a6246"); 107 | test_golden(seed) 108 | } 109 | -------------------------------------------------------------------------------- /src/bin/qql-cli.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::fmt::{Debug, Display}; 3 | use std::path::PathBuf; 4 | use std::str::FromStr; 5 | 6 | use clap::Parser; 7 | 8 | use qql::config::Animation; 9 | 10 | #[derive(Parser)] 11 | struct Opts { 12 | seed: Seed, 13 | 14 | /// Canvas width. 15 | /// 16 | /// This applies to the virtual canvas, before any viewport is computed. For instance, if 17 | /// `--width` is 1000 and `--viewport` is `0.5x0.5+0.25+0.25`, the actual output file will be 18 | /// 500px wide. 19 | #[clap(short, long, default_value = "2400")] 20 | width: i32, 21 | #[clap(short = 'o')] 22 | output_filename: Option, 23 | #[clap(flatten)] 24 | config: qql::config::Config, 25 | } 26 | 27 | #[derive(Copy, Clone)] 28 | struct Seed(pub [u8; 32]); 29 | impl Seed { 30 | fn as_bytes(&self) -> &[u8; 32] { 31 | &self.0 32 | } 33 | } 34 | impl FromStr for Seed { 35 | type Err = anyhow::Error; 36 | fn from_str(mut s: &str) -> Result { 37 | if s.starts_with("0x") { 38 | s = &s[2..]; 39 | } 40 | let bytes: Vec = hex::decode(s)?; 41 | let bytes: [u8; 32] = <[u8; 32]>::try_from(bytes) 42 | .map_err(|e| anyhow::anyhow!("Seed must be 32 bytes; got {}", e.len()))?; 43 | Ok(Seed(bytes)) 44 | } 45 | } 46 | impl Debug for Seed { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | f.write_str("0x")?; 49 | let mut buf = [0u8; 2 * 32]; 50 | hex::encode_to_slice(self.0, &mut buf).unwrap(); 51 | f.write_str(std::str::from_utf8(&buf).unwrap()) 52 | } 53 | } 54 | impl Display for Seed { 55 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 56 | ::fmt(self, f) 57 | } 58 | } 59 | 60 | fn main() { 61 | let opts = Opts::parse(); 62 | let color_db = qql::color::ColorDb::from_bundle(); 63 | 64 | if let (Animation::None, true) = (&opts.config.animate, opts.config.splatter_immediately) { 65 | eprintln!("fatal: --splatter-immediately does not apply unless --animate is also set"); 66 | std::process::exit(1); 67 | }; 68 | 69 | let base_filepath = if let Some(f) = opts.output_filename { 70 | f 71 | } else { 72 | let mut basename = opts.seed.to_string(); 73 | if opts.config.inflate_draw_radius { 74 | basename.push_str("-inflated"); 75 | } 76 | if opts.config.fast_collisions { 77 | basename.push_str("-fastcoll"); 78 | } 79 | basename.push_str(".png"); 80 | PathBuf::from(basename) 81 | }; 82 | 83 | let consume_frame = |frame: qql::art::Frame| { 84 | let filename = match frame.number { 85 | None => base_filepath.clone(), 86 | Some(n) => { 87 | let mut filename = base_filepath 88 | .file_stem() 89 | .unwrap_or(OsStr::new("")) 90 | .to_owned(); 91 | filename.push(format!("{:04}.", n)); 92 | let mut filename = PathBuf::from(filename); 93 | if let Some(ext) = base_filepath.extension() { 94 | filename.set_extension(ext); 95 | } 96 | base_filepath.with_file_name(filename) 97 | } 98 | }; 99 | if let Err(e) = frame.dt.write_png(&filename) { 100 | eprintln!("Failed to write PNG to {}: {}", filename.display(), e); 101 | std::process::exit(1); 102 | } 103 | match frame.number { 104 | None => eprintln!("wrote png: {}", filename.display()), 105 | Some(n) => eprintln!("wrote frame {}: {}", n, filename.display()), 106 | }; 107 | }; 108 | 109 | let render_data = qql::art::draw( 110 | opts.seed.as_bytes(), 111 | &color_db, 112 | &opts.config, 113 | opts.width, 114 | consume_frame, 115 | ); 116 | 117 | println!("num_points: {}", render_data.num_points); 118 | let color_names: Vec<&str> = render_data 119 | .colors_used 120 | .iter() 121 | .map(|k| { 122 | color_db 123 | .color(k) 124 | .map_or("", |c| c.name.as_str()) 125 | }) 126 | .collect(); 127 | println!("colors: {:?}", color_names); 128 | println!("ring counts: {:?}", render_data.ring_counts_used); 129 | } 130 | -------------------------------------------------------------------------------- /src/sectors.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use crate::{ 4 | config::Config, 5 | math::{dist, dist_lower_bound, dist_upper_bound}, 6 | }; 7 | 8 | const NUM_SECTORS: usize = 50; 9 | 10 | struct Indexer { 11 | start: f64, 12 | length: f64, 13 | } 14 | 15 | impl Indexer { 16 | pub fn new(start: f64, stop: f64) -> Self { 17 | let length = (stop - start) / NUM_SECTORS as f64; 18 | Indexer { start, length } 19 | } 20 | 21 | pub fn index(&self, value: f64) -> usize { 22 | let index = f64::floor((value - self.start) / self.length) as usize; 23 | index.clamp(0, NUM_SECTORS - 1) 24 | } 25 | } 26 | 27 | pub struct Sectors { 28 | fast_collisions: bool, 29 | ix: Indexer, 30 | iy: Indexer, 31 | sectors: Box<[[Vec; NUM_SECTORS]; NUM_SECTORS]>, 32 | } 33 | 34 | #[derive(Debug, Copy, Clone)] 35 | pub struct Collider { 36 | pub position: (f64, f64), 37 | pub radius: f64, 38 | } 39 | 40 | impl Sectors { 41 | pub fn new(config: &Config, left: f64, right: f64, top: f64, bottom: f64) -> Self { 42 | let ix = Indexer::new(f64::min(left, right), f64::max(left, right)); 43 | let iy = Indexer::new(f64::min(top, bottom), f64::max(top, bottom)); 44 | let sectors = { 45 | const EMPTY_SECTOR: Vec = Vec::new(); 46 | let vec_of_arrays = vec![[EMPTY_SECTOR; NUM_SECTORS]; NUM_SECTORS]; 47 | let slice_of_arrays = vec_of_arrays.into_boxed_slice(); 48 | slice_of_arrays.try_into().unwrap() 49 | }; 50 | Sectors { 51 | fast_collisions: config.fast_collisions, 52 | ix, 53 | iy, 54 | sectors, 55 | } 56 | } 57 | 58 | /// Tests whether the given collider can be included in this sector grid without any 59 | /// collisions, and adds it if it can. Returns whether the collider was added. 60 | pub fn test_and_add(&mut self, collider: Collider) -> bool { 61 | let Some(affected) = self.affected(&collider) else { 62 | // Colliders affecting no sectors never collide and don't need to be added to anything. 63 | // We can't actually iterate over this because colliders with very negative radii can 64 | // have `y_max > y_min` and thus panic in the slice dereference. 65 | return true; 66 | }; 67 | let fast_collisions = self.fast_collisions; 68 | for sector in self.sectors(&affected) { 69 | for other in sector { 70 | if collides(fast_collisions, &collider, other) { 71 | return false; 72 | } 73 | } 74 | } 75 | // OK: no collisions. 76 | for sector in self.sectors(&affected) { 77 | sector.push(collider); 78 | } 79 | true 80 | } 81 | 82 | /// Precondition: `collider.radius` must be non-negative. 83 | fn affected(&self, collider: &Collider) -> Option { 84 | let x_min = self.ix.index(collider.position.0 - collider.radius); 85 | let x_max = self.ix.index(collider.position.0 + collider.radius); 86 | if x_min > x_max { 87 | return None; 88 | } 89 | 90 | let y_min = self.iy.index(collider.position.1 - collider.radius); 91 | let y_max = self.iy.index(collider.position.1 + collider.radius); 92 | if y_min > y_max { 93 | return None; 94 | } 95 | 96 | Some(Affected { 97 | xs: x_min..=x_max, 98 | ys: y_min..=y_max, 99 | }) 100 | } 101 | 102 | fn sectors(&mut self, affected: &Affected) -> impl Iterator> { 103 | let (x_min, x_max) = (*affected.xs.start(), *affected.xs.end()); 104 | let (y_min, y_max) = (*affected.ys.start(), *affected.ys.end()); 105 | self.sectors[x_min..=x_max] 106 | .iter_mut() 107 | .flat_map(move |row| row[y_min..=y_max].iter_mut()) 108 | } 109 | } 110 | 111 | /// Invariant: only constructed with `xs` and `ys` as well-formed ranges (i.e., that won't panic on 112 | /// slicing). 113 | #[derive(Debug)] 114 | struct Affected { 115 | xs: RangeInclusive, 116 | ys: RangeInclusive, 117 | } 118 | 119 | fn collides(fast_collisions: bool, c1: &Collider, c2: &Collider) -> bool { 120 | if fast_collisions { 121 | collides_fast(c1, c2) 122 | } else { 123 | collides_dist(c1, c2) 124 | } 125 | } 126 | 127 | fn collides_fast(c1: &Collider, c2: &Collider) -> bool { 128 | let (x1, y1) = c1.position; 129 | let (x2, y2) = c2.position; 130 | let radius = c1.radius + c2.radius; 131 | if radius < 0.0 { 132 | return false; 133 | } 134 | let dx = x1 - x2; 135 | let dy = y1 - y2; 136 | dx * dx + dy * dy <= radius * radius 137 | } 138 | 139 | fn collides_dist(c1: &Collider, c2: &Collider) -> bool { 140 | let p1 = c1.position; 141 | let p2 = c2.position; 142 | let radius = c1.radius + c2.radius; 143 | if dist_lower_bound(p1, p2) > radius { 144 | return false; 145 | } 146 | if dist_upper_bound(p1, p2) <= radius { 147 | return true; 148 | } 149 | dist(p1, p2) <= radius 150 | } 151 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Eq, Clone)] 2 | pub struct Traits { 3 | pub flow_field: FlowField, 4 | pub turbulence: Turbulence, 5 | pub margin: Margin, 6 | pub color_variety: ColorVariety, 7 | pub color_mode: ColorMode, 8 | pub structure: Structure, 9 | pub bullseye_rings: BullseyeRings, 10 | pub ring_thickness: RingThickness, 11 | pub ring_size: RingSize, 12 | pub size_variety: SizeVariety, 13 | pub color_palette: ColorPalette, 14 | pub spacing: Spacing, 15 | pub version: Version, 16 | } 17 | 18 | impl Traits { 19 | pub fn from_seed(raw_seed: &[u8; 32]) -> Self { 20 | let mut remaining = u32::from_be_bytes(raw_seed[28..32].try_into().unwrap()); 21 | fn pluck(seed: &mut u32, options: &'static [(T, u32)]) -> T { 22 | if options.is_empty() { 23 | panic!("no options"); 24 | } 25 | let num_bits: u32 = options.len().next_power_of_two().ilog2(); 26 | let mask: u32 = (1 << num_bits) - 1; 27 | let index = *seed & mask; 28 | *seed >>= num_bits; 29 | let (option, _weight) = options[(index % options.len() as u32) as usize]; 30 | option 31 | } 32 | Traits { 33 | flow_field: pluck(&mut remaining, FlowField::options()), 34 | turbulence: pluck(&mut remaining, Turbulence::options()), 35 | margin: pluck(&mut remaining, Margin::options()), 36 | color_variety: pluck(&mut remaining, ColorVariety::options()), 37 | color_mode: pluck(&mut remaining, ColorMode::options()), 38 | structure: pluck(&mut remaining, Structure::options()), 39 | bullseye_rings: { 40 | let ring_options = &[(true, 1), (false, 1)]; 41 | BullseyeRings { 42 | one: pluck(&mut remaining, ring_options), 43 | three: pluck(&mut remaining, ring_options), 44 | seven: pluck(&mut remaining, ring_options), 45 | } 46 | }, 47 | ring_thickness: pluck(&mut remaining, RingThickness::options()), 48 | ring_size: pluck(&mut remaining, RingSize::options()), 49 | size_variety: pluck(&mut remaining, SizeVariety::options()), 50 | color_palette: pluck(&mut remaining, ColorPalette::options()), 51 | spacing: pluck(&mut remaining, Spacing::options()), 52 | version: Traits::get_version(raw_seed), 53 | } 54 | } 55 | 56 | fn get_version(raw_seed: &[u8; 32]) -> Version { 57 | let sentinel = &raw_seed[26..28]; 58 | if sentinel != [0xff, 0xff] { 59 | return Version::Unversioned; 60 | } 61 | match raw_seed[28] >> 4 { 62 | 0 => Version::V0, 63 | 1 => Version::V1, 64 | _ => Version::Unversioned, 65 | } 66 | } 67 | } 68 | 69 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 70 | pub enum Version { 71 | Unversioned, 72 | V0, 73 | V1, 74 | } 75 | 76 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 77 | pub struct BullseyeRings { 78 | pub one: bool, 79 | pub three: bool, 80 | pub seven: bool, 81 | } 82 | 83 | macro_rules! trait_enum { 84 | ($trait:ident { $($value:ident($weight:expr)),* $(,)? }) => { 85 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 86 | pub enum $trait { 87 | $($value),* 88 | } 89 | impl $trait { 90 | pub fn options() -> &'static [(Self, u32)] { 91 | &[$((Self::$value, $weight)),*] 92 | } 93 | } 94 | }; 95 | } 96 | 97 | trait_enum!(FlowField { 98 | Horizontal(3), 99 | Diagonal(1), 100 | Vertical(3), 101 | RandomLinear(1), 102 | Explosive(1), 103 | Spiral(4), 104 | Circular(2), 105 | RandomRadial(1), 106 | }); 107 | 108 | trait_enum!(Turbulence { 109 | None(0), 110 | Low(3), 111 | High(1), 112 | }); 113 | 114 | trait_enum!(Margin { 115 | None(1), 116 | Crisp(1), 117 | Wide(2), 118 | }); 119 | 120 | trait_enum!(ColorVariety { 121 | Low(2), 122 | Medium(4), 123 | High(3), 124 | }); 125 | 126 | trait_enum!(ColorMode { 127 | Simple(2), 128 | Stacked(3), 129 | Zebra(1), 130 | }); 131 | 132 | trait_enum!(Structure { 133 | Orbital(1), 134 | Formation(1), 135 | Shadows(1), 136 | }); 137 | 138 | trait_enum!(RingThickness { 139 | Thin(1), 140 | Thick(2), 141 | Mixed(2), 142 | }); 143 | 144 | trait_enum!(SizeVariety { 145 | Constant(1), 146 | Variable(3), 147 | Wild(1), 148 | }); 149 | 150 | trait_enum!(RingSize { 151 | Small(4), 152 | Medium(3), 153 | Large(1), 154 | }); 155 | 156 | trait_enum!(ColorPalette { 157 | Austin(1), 158 | Berlin(1), 159 | Edinburgh(2), 160 | Fidenza(2), 161 | Miami(1), 162 | Seattle(1), 163 | Seoul(2), 164 | }); 165 | 166 | trait_enum!(Spacing { 167 | Dense(2), 168 | Medium(1), 169 | Sparse(1), 170 | }); 171 | 172 | #[cfg(test)] 173 | mod test { 174 | use super::*; 175 | use hex_literal::hex; 176 | 177 | #[test] 178 | fn test_decode_qql_1() { 179 | let raw_seed = &hex!("33c9371d25ce44a408f8a6473fbad86bf81e1a178c012cd49a85ffff14c54b46"); 180 | let traits = Traits::from_seed(raw_seed); 181 | assert_eq!( 182 | traits, 183 | Traits { 184 | flow_field: FlowField::Circular, 185 | turbulence: Turbulence::None, 186 | margin: Margin::Wide, 187 | color_variety: ColorVariety::High, 188 | color_mode: ColorMode::Stacked, 189 | structure: Structure::Formation, 190 | bullseye_rings: BullseyeRings { 191 | one: true, 192 | three: false, 193 | seven: true, 194 | }, 195 | ring_thickness: RingThickness::Thick, 196 | ring_size: RingSize::Medium, 197 | size_variety: SizeVariety::Constant, 198 | color_palette: ColorPalette::Fidenza, 199 | spacing: Spacing::Sparse, 200 | version: Version::V1, 201 | } 202 | ); 203 | } 204 | 205 | /// This seed has asymmetrical bullseye rings (1 and 3 but not 7), and is also unversioned 206 | /// (generated before the spirals patch). 207 | #[test] 208 | fn test_decode_qql_2() { 209 | let raw_seed = &hex!("e03a5189dac8182085e4adf66281f679fff2291d52a252d295b02feda9118a49"); 210 | let traits = Traits::from_seed(raw_seed); 211 | assert_eq!( 212 | traits, 213 | Traits { 214 | flow_field: FlowField::Diagonal, 215 | turbulence: Turbulence::Low, 216 | margin: Margin::Wide, 217 | color_variety: ColorVariety::Low, 218 | color_mode: ColorMode::Stacked, 219 | structure: Structure::Formation, 220 | bullseye_rings: BullseyeRings { 221 | one: true, 222 | three: true, 223 | seven: false, 224 | }, 225 | ring_thickness: RingThickness::Thick, 226 | ring_size: RingSize::Small, 227 | size_variety: SizeVariety::Variable, 228 | color_palette: ColorPalette::Miami, 229 | spacing: Spacing::Dense, 230 | version: Version::Unversioned, 231 | } 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{hash_map::Entry::*, BTreeMap, HashMap}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | const COLORS_JSON: &str = include_str!("colordata.json"); 6 | 7 | #[derive(Debug, Deserialize, Serialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct ColorSpec { 10 | pub name: String, 11 | pub hue_variance: f64, 12 | pub sat_variance: f64, 13 | pub bright_variance: f64, 14 | pub hue: f64, 15 | pub hue_min: f64, 16 | pub hue_max: f64, 17 | pub sat: f64, 18 | pub sat_min: f64, 19 | pub sat_max: f64, 20 | pub bright: f64, 21 | pub bright_min: f64, 22 | pub bright_max: f64, 23 | } 24 | 25 | #[derive(Debug, Deserialize, Serialize)] 26 | #[serde(rename_all = "camelCase")] 27 | pub struct WirePaletteSpec { 28 | name: String, 29 | swatches: Vec, 30 | color_seq: Vec, 31 | background_colors: Vec, 32 | splatter_colors: Vec, 33 | } 34 | 35 | #[derive(Debug, Deserialize, Serialize)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct WireBackgroundColorSpec( 38 | /// Name. 39 | String, 40 | /// Weight. 41 | f64, 42 | /// Substitutions. 43 | BTreeMap>, 44 | ); 45 | 46 | #[derive(Debug, Deserialize, Serialize)] 47 | #[serde(rename_all = "camelCase")] 48 | pub struct WireSplatterColorSpec( 49 | /// Name. 50 | String, 51 | /// Weight. 52 | f64, 53 | ); 54 | 55 | #[derive(Debug, Deserialize, Serialize)] 56 | pub struct WireColorDb { 57 | colors: Vec, 58 | palettes: Vec, 59 | } 60 | 61 | pub type ColorKey = u32; 62 | 63 | #[derive(Debug, Serialize)] 64 | pub struct PaletteSpec { 65 | pub swatches: Vec, 66 | pub color_seq: Vec, 67 | pub background_colors: Vec<(BackgroundColorSpec, f64)>, 68 | pub splatter_colors: Vec<(ColorKey, f64)>, 69 | } 70 | 71 | #[derive(Debug, Serialize)] 72 | pub struct BackgroundColorSpec { 73 | pub color: ColorKey, 74 | pub substitutions: HashMap>, 75 | } 76 | 77 | #[derive(Debug, Serialize)] 78 | pub struct ColorDb { 79 | colors: Vec, 80 | colors_by_name: HashMap, 81 | palettes: HashMap, 82 | } 83 | 84 | #[derive(Debug)] 85 | pub enum WireFormatError { 86 | TooManyColors, 87 | DuplicateColor { name: String }, 88 | DuplicatePalette { name: String }, 89 | UndefinedColor { name: String, palette: String }, 90 | } 91 | 92 | impl ColorDb { 93 | pub fn from_bundle() -> Self { 94 | let wire: WireColorDb = 95 | serde_json::from_str(COLORS_JSON).expect("bundled data is invalid JSON"); 96 | ColorDb::from_wire(wire).expect("bundled data is not a valid database") 97 | } 98 | 99 | pub fn from_wire(wire: WireColorDb) -> Result { 100 | let mut db = ColorDb { 101 | colors: Vec::with_capacity(wire.colors.len()), 102 | colors_by_name: HashMap::with_capacity(wire.colors.len()), 103 | palettes: HashMap::with_capacity(wire.palettes.len()), 104 | }; 105 | 106 | for color in wire.colors { 107 | let color_key = 108 | ColorKey::try_from(db.colors.len()).map_err(|_| WireFormatError::TooManyColors)?; 109 | let name = color.name.clone(); 110 | db.colors.push(color); 111 | match db.colors_by_name.entry(name) { 112 | Occupied(o) => { 113 | let name = o.remove_entry().0; 114 | return Err(WireFormatError::DuplicateColor { name }); 115 | } 116 | Vacant(v) => v.insert(color_key), 117 | }; 118 | } 119 | 120 | for palette in wire.palettes { 121 | let palette_entry = match db.palettes.entry(palette.name) { 122 | Occupied(o) => { 123 | let name = o.remove_entry().0; 124 | return Err(WireFormatError::DuplicatePalette { name }); 125 | } 126 | Vacant(v) => v, 127 | }; 128 | let find = |color: String| -> Result { 129 | Self::look_up(color, palette_entry.key(), &db.colors_by_name) 130 | }; 131 | let swatches = palette 132 | .swatches 133 | .into_iter() 134 | .map(find) 135 | .collect::, WireFormatError>>()?; 136 | let color_seq = palette 137 | .color_seq 138 | .into_iter() 139 | .map(find) 140 | .collect::, WireFormatError>>()?; 141 | let background_colors = palette 142 | .background_colors 143 | .into_iter() 144 | .map(|WireBackgroundColorSpec(bg, weight, substitutions)| { 145 | let substitutions = substitutions 146 | .into_iter() 147 | .map(|(from, maybe_to)| { 148 | let from: ColorKey = find(from)?; 149 | let maybe_to = maybe_to.map(find).transpose()?; 150 | Ok((from, maybe_to)) 151 | }) 152 | .collect::>, WireFormatError>>( 153 | )?; 154 | let spec = BackgroundColorSpec { 155 | color: find(bg)?, 156 | substitutions, 157 | }; 158 | Ok((spec, weight)) 159 | }) 160 | .collect::, WireFormatError>>()?; 161 | let splatter_colors = palette 162 | .splatter_colors 163 | .into_iter() 164 | .map(|WireSplatterColorSpec(c, weight)| Ok((find(c)?, weight))) 165 | .collect::, WireFormatError>>()?; 166 | palette_entry.insert(PaletteSpec { 167 | swatches, 168 | color_seq, 169 | background_colors, 170 | splatter_colors, 171 | }); 172 | } 173 | 174 | Ok(db) 175 | } 176 | 177 | fn look_up( 178 | name: String, 179 | palette: &str, 180 | map: &HashMap, 181 | ) -> Result { 182 | match map.get(&name) { 183 | Some(v) => Ok(*v), 184 | None => Err(WireFormatError::UndefinedColor { 185 | name, 186 | palette: palette.into(), 187 | }), 188 | } 189 | } 190 | 191 | pub fn color(&self, key: ColorKey) -> Option<&ColorSpec> { 192 | self.colors.get(key as usize) 193 | } 194 | 195 | pub fn color_by_name(&self, name: &str) -> Option<&ColorSpec> { 196 | self.color(*self.colors_by_name.get(name)?) 197 | } 198 | 199 | pub fn palette(&self, palette_key: crate::traits::ColorPalette) -> Option<&PaletteSpec> { 200 | use crate::traits::ColorPalette::*; 201 | let name = match palette_key { 202 | Austin => "austin", 203 | Berlin => "berlin", 204 | Edinburgh => "edinburgh", 205 | Fidenza => "fidenza", 206 | Miami => "miami", 207 | Seattle => "seattle", 208 | Seoul => "seoul", 209 | }; 210 | self.palettes.get(name) 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod test { 216 | use super::*; 217 | 218 | #[test] 219 | fn test_color_db_from_bundle() { 220 | let db = ColorDb::from_bundle(); 221 | assert!(!db.colors.is_empty()); 222 | assert!(!db.palettes.is_empty()); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, num::NonZeroU32, str::FromStr}; 2 | 3 | use anyhow::Context; 4 | 5 | #[derive(Debug, Default, clap::Args)] 6 | pub struct Config { 7 | /// Speed up collision checking by avoiding our slow `sqrt` implementation. May slightly 8 | /// affect layout. 9 | #[clap(long)] 10 | pub fast_collisions: bool, 11 | 12 | /// At paint time, ensure that all points have at least a small positive radius. 13 | #[clap(long)] 14 | pub inflate_draw_radius: bool, 15 | 16 | /// Use at least this many segments for every circle. Values below `8` have no effect. 17 | /// 18 | /// At typical resolutions, circles should look smooth without tweaking. But at very large 19 | /// resolutions (say, above 10k pixels wide), the segments may start to become visible, 20 | /// especially on small circles. Crank this value up linearly to compensate, at the cost of 21 | /// render time. 22 | #[clap(long, value_name = "STEPS")] 23 | pub min_circle_steps: Option, 24 | 25 | /// Restrict rendering to a region of the canvas. 26 | /// 27 | /// Values are specified as floats from 0.0 (top/left) to 1.0 (bottom/right). For instance, 28 | /// `0.1x0.1+0.45+0.45` renders the center 1% of the canvas. See `--width` about how this 29 | /// affects the output image size. 30 | #[clap(long, value_name = "WxH+X+Y")] 31 | pub viewport: Option, 32 | 33 | /// Chunks for parallel rendering. 34 | #[clap(long, value_name = "WxH", default_value_t)] 35 | pub chunks: Chunks, 36 | 37 | /// Output multiple frames showing the construction of the piece. 38 | /// 39 | /// May be `none` for no animation, `groups` to paint one flow line group at a time, or 40 | /// `points:N` (where `N` is a positive integer) to show paint `N` points at a time. 41 | #[clap(long, default_value_t)] 42 | pub animate: Animation, 43 | 44 | /// Animate in splatter points immediately after their parents. 45 | /// 46 | /// By default, all splatters are deferred to the end of the animation. With this option, 47 | /// each splatter point is instead drawn immediately after the point that spawned it. This 48 | /// takes additional processing and may increase render time. Can only be used if `--animate` 49 | /// is also set. 50 | #[clap(long, default_value_t)] 51 | pub splatter_immediately: bool, 52 | } 53 | 54 | #[derive(Default, Debug, Clone)] 55 | pub enum Animation { 56 | #[default] 57 | None, 58 | Groups, 59 | Points { 60 | step: u32, 61 | }, 62 | } 63 | 64 | impl FromStr for Animation { 65 | type Err = anyhow::Error; 66 | 67 | fn from_str(s: &str) -> Result { 68 | if let Some(n) = s.strip_prefix("points:") { 69 | let step: u32 = n.parse().map_err(|e| { 70 | anyhow::anyhow!("Invalid number of points per frame {:?}: {}", n, e) 71 | })?; 72 | if step == 0 { 73 | anyhow::bail!("Must add at least 1 point per frame"); 74 | } 75 | return Ok(Animation::Points { step }); 76 | } 77 | match s { 78 | "none" => Ok(Animation::None), 79 | "groups" => Ok(Animation::Groups), 80 | "points" => { 81 | anyhow::bail!("Must specify how many points to add per frame, like \"points:100\"") 82 | } 83 | _ => anyhow::bail!("Invalid animation spec"), 84 | } 85 | } 86 | } 87 | 88 | impl Display for Animation { 89 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 90 | match self { 91 | Animation::None => f.write_str("none"), 92 | Animation::Groups => f.write_str("groups"), 93 | Animation::Points { step } => write!(f, "points:{}", step), 94 | } 95 | } 96 | } 97 | 98 | /// A viewport/crop specification in fractional space, where both axes range from `0.0` to `1.0`. 99 | #[derive(Debug, PartialEq, Clone)] 100 | pub struct FractionalViewport { 101 | width: f64, 102 | height: f64, 103 | left: f64, 104 | top: f64, 105 | } 106 | 107 | impl Default for FractionalViewport { 108 | fn default() -> Self { 109 | Self { 110 | width: 1.0, 111 | height: 1.0, 112 | left: 0.0, 113 | top: 0.0, 114 | } 115 | } 116 | } 117 | 118 | impl FractionalViewport { 119 | pub fn from_whlt(width: f64, height: f64, left: f64, top: f64) -> Self { 120 | Self { 121 | width, 122 | height, 123 | left, 124 | top, 125 | } 126 | } 127 | pub fn width(&self) -> f64 { 128 | self.width 129 | } 130 | pub fn height(&self) -> f64 { 131 | self.height 132 | } 133 | pub fn left(&self) -> f64 { 134 | self.left 135 | } 136 | pub fn top(&self) -> f64 { 137 | self.top 138 | } 139 | pub fn right(&self) -> f64 { 140 | self.left + self.width 141 | } 142 | pub fn bottom(&self) -> f64 { 143 | self.top + self.height 144 | } 145 | } 146 | 147 | /// Expects a string like `WxH+X+Y`, as with imagemagick geometry syntax. 148 | impl FromStr for FractionalViewport { 149 | type Err = anyhow::Error; 150 | 151 | fn from_str(s: &str) -> Result { 152 | fn parts(s: &str) -> Option<(&str, &str, &str, &str)> { 153 | let (width, s) = s.split_once('x')?; 154 | let (height, s) = s.split_once('+')?; 155 | let (left, top) = s.split_once('+')?; 156 | Some((width, height, left, top)) 157 | } 158 | let (width, height, left, top) = parts(s).context("Invalid format; expected WxH+X+Y")?; 159 | Ok(FractionalViewport { 160 | width: width.parse().context("Invalid width")?, 161 | height: height.parse().context("Invalid height")?, 162 | left: left.parse().context("Invalid x-offset")?, 163 | top: top.parse().context("Invalid y-offset")?, 164 | }) 165 | } 166 | } 167 | 168 | #[derive(Debug, PartialEq, Clone)] 169 | pub struct Chunks { 170 | pub w: NonZeroU32, 171 | pub h: NonZeroU32, 172 | } 173 | 174 | impl FromStr for Chunks { 175 | type Err = anyhow::Error; 176 | 177 | fn from_str(s: &str) -> Result { 178 | let (w, h) = s.split_once('x').context("Invalid format; expected WxH")?; 179 | let w: u32 = w.parse().context("Invalid width")?; 180 | let h: u32 = h.parse().context("Invalid height")?; 181 | let w = NonZeroU32::try_from(w).context("Chunk width cannot be zero")?; 182 | let h = NonZeroU32::try_from(h).context("Chunk height cannot be zero")?; 183 | Ok(Chunks { w, h }) 184 | } 185 | } 186 | 187 | impl Default for Chunks { 188 | fn default() -> Self { 189 | let one = NonZeroU32::new(1).unwrap(); 190 | Chunks { w: one, h: one } 191 | } 192 | } 193 | 194 | impl Display for Chunks { 195 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 196 | write!(f, "{}x{}", self.w, self.h) 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | 204 | #[test] 205 | fn test_viewport_fromstr_ok() { 206 | assert_eq!( 207 | "100x200+30+60".parse::().unwrap(), 208 | FractionalViewport { 209 | width: 100.0, 210 | height: 200.0, 211 | left: 30.0, 212 | top: 60.0 213 | } 214 | ); 215 | } 216 | 217 | #[test] 218 | fn test_viewport_fromstr_errs() { 219 | fn check(input: &str, expected_err: &str) { 220 | let msg = input.parse::().unwrap_err().to_string(); 221 | assert_eq!(msg, expected_err); 222 | } 223 | check("100x200+30", "Invalid format; expected WxH+X+Y"); 224 | check("FOOx200+30+60", "Invalid width"); 225 | check("100xBAR+30+60", "Invalid height"); 226 | check("100x200+BAZ+60", "Invalid x-offset"); 227 | check("100x200+30+QUUX", "Invalid y-offset"); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/layouts.rs: -------------------------------------------------------------------------------- 1 | use super::art::{h, w}; 2 | use super::math::{add_polar_offset, dist, pi}; 3 | use super::rand::Rng; 4 | use super::traits::Structure; 5 | 6 | #[derive(Debug)] 7 | pub struct StartPointGroups(pub Vec>); 8 | 9 | impl StartPointGroups { 10 | pub fn build(structure: Structure, rng: &mut Rng) -> StartPointGroups { 11 | match structure { 12 | Structure::Orbital => orbital(rng), 13 | Structure::Shadows => shadows(rng), 14 | Structure::Formation => formation(rng), 15 | } 16 | } 17 | } 18 | 19 | fn orbital(rng: &mut Rng) -> StartPointGroups { 20 | let base_step = *rng.wc(&[ 21 | (w(0.01), 3.0), 22 | (w(0.02), 2.0), 23 | (w(0.04), 1.0), 24 | (w(0.06), 1.0), 25 | (w(0.08), 1.0), 26 | (w(0.16), 0.5), 27 | ]); 28 | let radial_step = base_step * 0.5; 29 | 30 | let radial_group_step = *rng.wc(&[(w(0.07), 0.333), (w(0.15), 0.333), (w(0.3), 0.333)]); 31 | 32 | let center_x = *rng.wc(&[ 33 | (w(0.5), 0.3), 34 | (w(0.333), 0.2), 35 | (w(0.666), 0.2), 36 | (w(-0.333), 0.1), 37 | (w(1.333), 0.1), 38 | (w(-1.6), 0.05), 39 | (w(1.6), 0.05), 40 | ]); 41 | let center_y = *rng.wc(&[ 42 | (h(0.5), 0.3), 43 | (h(0.333), 0.2), 44 | (h(0.666), 0.2), 45 | (h(-0.333), 0.1), 46 | (h(1.333), 0.1), 47 | (h(-1.6), 0.05), 48 | (h(1.6), 0.05), 49 | ]); 50 | let center = (center_x, center_y); 51 | 52 | let h0 = h(-1.0 / 3.0); 53 | let h1 = h(4.0 / 3.0); 54 | let w0 = w(-1.0 / 3.0); 55 | let w1 = w(4.0 / 3.0); 56 | let in_bounds = |(x, y)| x > w0 && x < w1 && y > h0 && y < h1; 57 | 58 | let max_radius = w(0.05) 59 | + (0.0f64) 60 | .max(dist((center_x, center_y), (0.0, 0.0))) 61 | .max(dist((center_x, center_y), (w(1.0), 0.0))) 62 | .max(dist((center_x, center_y), (w(1.0), h(1.0)))) 63 | .max(dist((center_x, center_y), (0.0, h(1.0)))); 64 | let split_offset = rng.uniform(0.0, pi(2.0)); 65 | 66 | let mut groups = Vec::new(); 67 | let mut group_radius = w(0.001); 68 | while group_radius < max_radius { 69 | let num_splits = *rng.choice(&[1, 2, 3]); 70 | let split_len = pi(2.0) / num_splits as f64; 71 | 72 | let mut theta = split_offset; 73 | while theta < split_offset + pi(2.0) { 74 | let mut group = Vec::new(); 75 | let mut radius = group_radius; 76 | while radius < group_radius + radial_group_step { 77 | let circumference = radius * pi(2.0); 78 | let steps_wanted = circumference / base_step; 79 | let theta_step = f64::max(pi(0.005), pi(2.0) / steps_wanted); 80 | let mut inner_theta = theta; 81 | while inner_theta < theta + split_len { 82 | let point = add_polar_offset(center, inner_theta, radius); 83 | if in_bounds(point) { 84 | group.push(point); 85 | } 86 | inner_theta += theta_step; 87 | } 88 | radius += radial_step; 89 | } 90 | groups.push(group); 91 | theta += split_len; 92 | } 93 | group_radius += radial_group_step; 94 | } 95 | StartPointGroups(groups) 96 | } 97 | 98 | fn shadows(rng: &mut Rng) -> StartPointGroups { 99 | let num_circles = *rng.choice(&[5, 7, 10, 20, 30, 60]); 100 | struct Circle { 101 | center: (f64, f64), 102 | radius: f64, 103 | } 104 | 105 | let p_square = *rng.choice(&[0.0, 0.5, 1.0]); 106 | let columnar_square = rng.odds(0.5); 107 | let outward_radial = rng.odds(0.5); 108 | 109 | let collides = |c1: &Circle, c2: &Circle| { 110 | let d = dist(c1.center, c2.center); 111 | d < c1.radius + c2.radius 112 | }; 113 | 114 | let radial_fill = |c: Circle, rng: &mut Rng| -> Vec<(f64, f64)> { 115 | let radius_step = w(0.02); 116 | let circumference_step = w(0.01); 117 | let mut radius = c.radius; 118 | let mut group = Vec::new(); 119 | while radius > 0.0 { 120 | let num_steps = (radius * pi(2.0)) / circumference_step; 121 | let theta_step = pi(2.0) / num_steps; 122 | let mut theta = 0.0; 123 | while theta < pi(2.01) { 124 | group.push(add_polar_offset(c.center, theta, radius)); 125 | theta += theta_step; 126 | } 127 | radius -= radius_step; 128 | } 129 | if outward_radial { 130 | group.reverse(); 131 | } 132 | if rng.odds(0.05) { 133 | group = rng.shuffle(group); 134 | } 135 | group 136 | }; 137 | 138 | let square_fill = |c: Circle, rng: &mut Rng| -> Vec<(f64, f64)> { 139 | let step = *rng.wc(&[ 140 | (w(0.0075), 0.37), 141 | (w(0.01), 0.35), 142 | (w(0.02), 0.25), 143 | (w(0.04), 0.02), 144 | (w(0.08), 0.01), 145 | ]); 146 | let radius = c.radius; 147 | let r2 = radius * radius; 148 | let mut group = Vec::new(); 149 | let mut offset1 = -radius; 150 | while offset1 < radius { 151 | let mut offset2 = -radius; 152 | while offset2 < radius { 153 | let (x, y) = if columnar_square { 154 | (c.center.0 + offset1, c.center.1 + offset2) 155 | } else { 156 | (c.center.0 + offset2, c.center.1 + offset1) 157 | }; 158 | let dx = c.center.0 - x; 159 | let dy = c.center.1 - y; 160 | if dx * dx + dy * dy < r2 { 161 | group.push((x, y)); 162 | } 163 | offset2 += step; 164 | } 165 | offset1 += step; 166 | } 167 | group 168 | }; 169 | 170 | let fill = |c: Circle, rng: &mut Rng| { 171 | if rng.odds(p_square) { 172 | square_fill(c, rng) 173 | } else { 174 | radial_fill(c, rng) 175 | } 176 | }; 177 | 178 | let mut iter = 0; 179 | let mut circles = Vec::with_capacity(num_circles); 180 | while circles.len() < num_circles && iter < 1000 { 181 | iter += 1; 182 | let c = Circle { 183 | center: (rng.uniform(w(0.0), w(1.0)), rng.uniform(h(0.0), h(1.0))), 184 | radius: rng.uniform(w(0.05), w(0.5)), 185 | }; 186 | if circles.iter().all(|c2| !collides(&c, c2)) { 187 | circles.push(c); 188 | } 189 | } 190 | let groups = circles.into_iter().map(|c| fill(c, rng)).collect(); 191 | StartPointGroups(groups) 192 | } 193 | 194 | fn formation(rng: &mut Rng) -> StartPointGroups { 195 | let step = *rng.wc(&[ 196 | (w(0.0075), 0.37), 197 | (w(0.01), 0.35), 198 | (w(0.02), 0.25), 199 | (w(0.04), 0.02), 200 | (w(0.08), 0.01), 201 | ]); 202 | 203 | let num_horizontal_steps = *rng.wc(&[ 204 | (1, 0.7), 205 | (2, 0.35), 206 | (3, 0.25), 207 | (4, 0.1), 208 | (5, 0.05), 209 | (7, 0.05), 210 | ]); 211 | let num_vertical_steps = *rng.wc(&[ 212 | (1, 0.4), 213 | (2, 0.35), 214 | (3, 0.25), 215 | (4, 0.1), 216 | (5, 0.05), 217 | (7, 0.05), 218 | ]); 219 | 220 | let horizontal_step_len = w(1.2) / num_horizontal_steps as f64; 221 | let vertical_step_len = h(1.2) / num_vertical_steps as f64; 222 | 223 | let skip_odds = *rng.wc(&[(0.0, 0.5), (0.1, 0.3), (0.2, 0.15), (0.5, 0.05)]); 224 | 225 | let mut starting_chunks = Vec::with_capacity(num_horizontal_steps * num_vertical_steps); 226 | { 227 | let mut x = w(-0.1); 228 | while x < w(1.1) { 229 | let mut y = h(-0.1); 230 | while y < h(1.1) { 231 | starting_chunks.push((x, y)); 232 | y += vertical_step_len; 233 | } 234 | x += horizontal_step_len; 235 | } 236 | } 237 | let starting_chunks = rng.shuffle(starting_chunks); 238 | let starting_chunks = starting_chunks 239 | .into_iter() 240 | .enumerate() 241 | .filter(|&(i, _pt)| i == 0 || !rng.odds(skip_odds)) 242 | .map(|(_i, pt)| pt); 243 | 244 | let groups = starting_chunks 245 | .map(|(x_start, y_start)| { 246 | let mut group = Vec::new(); 247 | let mut y = y_start; 248 | while y < y_start + vertical_step_len { 249 | let mut x = x_start; 250 | while x < x_start + horizontal_step_len { 251 | group.push((x, y)); 252 | x += step; 253 | } 254 | y += step; 255 | } 256 | group 257 | }) 258 | .collect(); 259 | 260 | StartPointGroups(groups) 261 | } 262 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /src/math.rs: -------------------------------------------------------------------------------- 1 | use std::f64::consts::PI; 2 | 3 | #[inline(always)] 4 | pub fn pi(v: f64) -> f64 { 5 | PI * v 6 | } 7 | 8 | pub fn modulo(n: f64, m: f64) -> f64 { 9 | // Properly, this would be just `n.rem_euclid(m)`, but the QQL implementation behaves 10 | // differently when *both* `n` and `m` are negative. Out of an abundance of caution, we copy 11 | // that implementation, even though it's slower. 12 | ((n % m) + m) % m 13 | } 14 | 15 | pub fn rescale(value: f64, (old_min, old_max): (f64, f64), (new_min, new_max): (f64, f64)) -> f64 { 16 | let clamped = value.clamp(old_min, old_max); 17 | let old_spread = old_max - old_min; 18 | let new_spread = new_max - new_min; 19 | new_min + (clamped - old_min) * (new_spread / old_spread) 20 | } 21 | 22 | /// If `table` is the image of `linspace(min, max, _)` under a function `f` whose period divides 23 | /// `max - min`, then `interpolate(table, min, max, z)` approximates `f(z)`. 24 | fn interpolate(table: &[f64], min: f64, max: f64, z: f64) -> f64 { 25 | // Coerce value to [min, max) assuming periodicity. 26 | let value = modulo(z - min, max - min) + min; 27 | 28 | let rescaled = rescale(value, (min, max), (0.0, (table.len() - 1) as f64)); 29 | let index = rescaled.floor() as usize; // This is within [0, table.length - 1). 30 | let fraction = rescaled - index as f64; // This is within [0, 1). 31 | 32 | // Function evaluated at value is within [start, end) based on index. 33 | let start = table[index]; 34 | let end = table[index + 1]; 35 | 36 | // Interpolate within [start, end) using fractional part. 37 | start + (end - start) * fraction 38 | } 39 | 40 | /// Piecewise-linear approximation to [`f64::cos`]. 41 | pub fn cos(z: f64) -> f64 { 42 | const TABLE: &[f64] = &[ 43 | 1.0, 0.99179, 0.96729, 0.92692, 0.87132, 0.80141, 0.71835, 0.62349, 0.51839, 0.40478, 44 | 0.28453, 0.1596, 0.03205, -0.09602, -0.22252, -0.34537, -0.46254, -0.57212, -0.6723, 45 | -0.76145, -0.83809, -0.90097, -0.94906, -0.98156, -0.99795, -0.99795, -0.98156, -0.94906, 46 | -0.90097, -0.83809, -0.76145, -0.6723, -0.57212, -0.46254, -0.34537, -0.22252, -0.09602, 47 | 0.03205, 0.1596, 0.28453, 0.40478, 0.51839, 0.62349, 0.71835, 0.80141, 0.87132, 0.92692, 48 | 0.96729, 0.99179, 1.0, 49 | ]; 50 | interpolate(TABLE, 0.0, 2.0 * PI, z) 51 | } 52 | 53 | /// Piecewise-linear approximation to [`f64::sin`]. 54 | pub fn sin(z: f64) -> f64 { 55 | const TABLE: &[f64] = &[ 56 | 0.0, 0.12788, 0.25365, 0.37527, 0.49072, 0.59811, 0.69568, 0.78183, 0.85514, 0.91441, 57 | 0.95867, 0.98718, 0.99949, 0.99538, 0.97493, 0.93847, 0.8866, 0.82017, 0.74028, 0.64823, 58 | 0.54553, 0.43388, 0.31511, 0.19116, 0.06407, -0.06407, -0.19116, -0.31511, -0.43388, 59 | -0.54553, -0.64823, -0.74028, -0.82017, -0.8866, -0.93847, -0.97493, -0.99538, -0.99949, 60 | -0.98718, -0.95867, -0.91441, -0.85514, -0.78183, -0.69568, -0.59811, -0.49072, -0.37527, 61 | -0.25365, -0.12788, -0.0, 62 | ]; 63 | interpolate(TABLE, 0.0, 2.0 * PI, z) 64 | } 65 | 66 | /// Approximation to [`f64::sqrt`] using Newton's method with fixed convergence parameters. 67 | fn sqrt(value: f64) -> f64 { 68 | const MAX_ITERATIONS: usize = 1000; 69 | const EPSILON: f64 = 1e-14; 70 | const TARGET: f64 = 1e-7; 71 | 72 | if value < 0.0 { 73 | panic!("argument to sqrt must be non-negative"); 74 | } 75 | 76 | let mut guess = value; 77 | for _ in 0..MAX_ITERATIONS { 78 | let error = guess * guess - value; 79 | if error.abs() < TARGET { 80 | return guess; 81 | } 82 | let divisor = 2.0 * guess; 83 | if divisor <= EPSILON { 84 | return guess; 85 | } 86 | guess -= error / divisor; 87 | } 88 | guess 89 | } 90 | 91 | /// Computes the distance between two points. 92 | /// 93 | /// Approximately like [`f64::hypot(x2 - x1, y2 - y1)`][f64::hypot]. 94 | pub fn dist((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 { 95 | let dx = x1 - x2; 96 | let dy = y1 - y2; 97 | sqrt(dx * dx + dy * dy) 98 | } 99 | 100 | /// Fast lower-bound approximation of [`dist`]. 101 | /// 102 | /// **WARNING:** In the JavaScript source, the function `distLowerBound` actually implements an 103 | /// upper bound, and vice versa. This implementation of `dist_lower_bound` corresponds to the 104 | /// JavaScript function `distUpperBound`. 105 | pub fn dist_lower_bound((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 { 106 | let dx = (x1 - x2).abs(); 107 | let dy = (y1 - y2).abs(); 108 | let min = f64::min(dx, dy); 109 | let max = f64::max(dx, dy); 110 | 111 | let alpha = 1007.0 / 1110.0; 112 | let beta = 441.0 / 1110.0; 113 | 114 | alpha * max + beta * min 115 | } 116 | 117 | /// Fast upper-bound approximation of [`dist`]. 118 | /// 119 | /// **WARNING:** In the JavaScript source, the function `distUpperBound` actually implements a 120 | /// lower bound, and vice versa. This implementation of `dist_upper_bound` corresponds to the 121 | /// JavaScript function `distLowerBound`. 122 | pub fn dist_upper_bound((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 { 123 | let dx = (x1 - x2).abs(); 124 | let dy = (y1 - y2).abs(); 125 | let min = f64::min(dx, dy); 126 | let max = f64::max(dx, dy); 127 | 128 | let beta = 441.0 / 1024.0; 129 | 130 | max + beta * min 131 | } 132 | 133 | /// "Fast" atan2 implementation using a polynomial approximation. 134 | /// Adapted from . 135 | fn atan2(y: f64, x: f64) -> f64 { 136 | let ax = x.abs(); 137 | let ay = y.abs(); 138 | let mx = f64::max(ay, ax); 139 | let mn = f64::min(ay, ax); 140 | let a = mn / mx; 141 | 142 | // Minimax polynomial approximation to atan(a) on [0,1] 143 | let s = a * a; 144 | let c = s * a; 145 | let q = s * s; 146 | let mut r = 0.024840285 * q + 0.18681418; 147 | let t = -0.094097948 * q - 0.33213072; 148 | r = r * s + t; 149 | r = r * c + a; 150 | 151 | // Map to full circle 152 | if ay > ax { 153 | r = 1.57079637 - r; 154 | } 155 | if x < 0.0 { 156 | r = 3.14159274 - r; 157 | } 158 | if y < 0.0 { 159 | r = -r; 160 | } 161 | r 162 | } 163 | 164 | /// Computes the angle from `(x1, y1)` to `(x2, y2)`, as a value in radians from 0 to 2π. 165 | pub fn angle((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 { 166 | let a = atan2(y2 - y1, x2 - x1); 167 | modulo(a, pi(2.0)) 168 | } 169 | 170 | pub fn add_polar_offset((x, y): (f64, f64), theta: f64, r: f64) -> (f64, f64) { 171 | (x + r * cos(theta), y + r * sin(theta)) 172 | } 173 | 174 | #[cfg(test)] 175 | mod tests { 176 | use super::*; 177 | 178 | #[test] 179 | fn test_pi() { 180 | assert_eq!(pi(0.0), 0.0); 181 | assert_eq!(pi(1.0), PI); 182 | assert_eq!(pi(-3.7), -3.7 * PI); 183 | assert!(pi(f64::NAN).is_nan()); 184 | } 185 | 186 | #[test] 187 | fn test_modulo() { 188 | let a = 4.0; 189 | let b = 3.0; 190 | let z = a % b; 191 | 192 | assert_eq!(modulo(a, b), z); 193 | assert_eq!(modulo(a + 10.0 * b, b), z); 194 | assert_eq!(modulo(a - 10.0 * b, b), z); 195 | 196 | assert_eq!(modulo(a, -b), z - b); 197 | assert_eq!(modulo(a + 10.0 * b, -b), z - b); 198 | assert_eq!(modulo(a - 10.0 * b, -b), z - b); 199 | } 200 | 201 | #[test] 202 | fn test_rescale() { 203 | assert_eq!(rescale(2.0625, (1.0625, 5.0625), (10.0, 20.0)), 12.5); 204 | } 205 | 206 | #[test] 207 | fn test_sin_cos() { 208 | const TEST_CASES: &'static [(f64, f64, f64)] = &[ 209 | (0.0, 0.0, 1.0), 210 | (0.1, 0.09972839720069836, 0.9935973557943562), 211 | (1.0, 0.8403747950252756, 0.5395579585710483), 212 | (1.5707963267948966, 0.9984625, 0.00003250000000000475), 213 | (2.0, 0.9074940439786922, -0.41534209884358286), 214 | (3.141592653589793, 0.0, -0.99795), 215 | (6.283185307179586, 0.0, 1.0), 216 | (10.0, -0.5439582041429563, -0.8389752174069942), 217 | (-0.1, -0.09972839720069898, 0.993597355794356), 218 | (-1.0, -0.8403747950252756, 0.5395579585710483), 219 | ]; 220 | for &(z, sin_z, cos_z) in TEST_CASES { 221 | let actual_sin_z = sin(z); 222 | let actual_cos_z = cos(z); 223 | if sin_z != actual_sin_z { 224 | panic!("sin({}): got {}, want {}", z, actual_sin_z, sin_z); 225 | } 226 | if cos_z != actual_cos_z { 227 | panic!("cos({}): got {}, want {}", z, actual_cos_z, cos_z); 228 | } 229 | } 230 | 231 | let actual_sin_nan = sin(f64::NAN); 232 | let actual_cos_nan = cos(f64::NAN); 233 | if !actual_sin_nan.is_nan() { 234 | panic!("sin(NaN): got {}, want NaN", actual_sin_nan); 235 | } 236 | if !actual_cos_nan.is_nan() { 237 | panic!("cos(NaN): got {}, want NaN", actual_cos_nan); 238 | } 239 | } 240 | 241 | #[test] 242 | fn test_sqrt() { 243 | const TEST_CASES: &'static [(f64, f64)] = &[ 244 | (0.0, 0.0), 245 | (0.1, 0.3162277665175675), 246 | (1.0, 1.0), 247 | (2.0, 1.4142135623746899), 248 | (3456.789, 58.79446402511048), 249 | ]; 250 | for &(z, sqrt_z) in TEST_CASES { 251 | let actual_sqrt_z = sqrt(z); 252 | if sqrt_z != actual_sqrt_z { 253 | panic!("sqrt({}): got {}, want {}", z, actual_sqrt_z, sqrt_z); 254 | } 255 | } 256 | 257 | let actual_sqrt_nan = sqrt(f64::NAN); 258 | if !actual_sqrt_nan.is_nan() { 259 | panic!("sqrt(NaN): got {}, want NaN", actual_sqrt_nan); 260 | } 261 | } 262 | 263 | #[test] 264 | fn test_dist_and_bounds() { 265 | struct TestCase { 266 | points: ((f64, f64), (f64, f64)), 267 | lb: f64, 268 | d: f64, 269 | ub: f64, 270 | } 271 | const TEST_CASES: &'static [TestCase] = &[ 272 | TestCase { 273 | points: ((0.0, 0.0), (3.0, 4.0)), 274 | lb: 4.82072072072072, 275 | d: 5.000000000053723, 276 | ub: 5.2919921875, 277 | }, 278 | TestCase { 279 | points: ((1.0, 2.0), (3.0, 4.0)), 280 | lb: 2.609009009009009, 281 | d: 2.8284271250498643, 282 | ub: 2.861328125, 283 | }, 284 | TestCase { 285 | points: ((10.0, 20.0), (15.0, 32.0)), 286 | lb: 12.872972972972972, 287 | d: 13.0, 288 | ub: 14.1533203125, 289 | }, 290 | ]; 291 | for &TestCase { points, lb, d, ub } in TEST_CASES { 292 | let (p1, p2) = points; 293 | let actual_lb = dist_lower_bound(p1, p2); 294 | let actual_d = dist(p1, p2); 295 | let actual_ub = dist_upper_bound(p1, p2); 296 | if (lb, d, ub) != (actual_lb, actual_d, actual_ub) { 297 | panic!( 298 | "{:?} ~> {:?}: got {} .. {} .. {}, want {} .. {} .. {}", 299 | p1, p2, actual_lb, actual_d, actual_ub, lb, d, ub 300 | ); 301 | } 302 | if !(actual_ub >= actual_d) || !(actual_lb <= actual_d) { 303 | panic!( 304 | "{:?} ~> {:?}: bad bounds: {} .. {} .. {}", 305 | p1, p2, actual_lb, actual_d, actual_ub 306 | ); 307 | } 308 | } 309 | } 310 | 311 | #[test] 312 | fn test_angle() { 313 | struct TestCase { 314 | points: ((f64, f64), (f64, f64)), 315 | angle: f64, 316 | } 317 | const TEST_CASES: &'static [TestCase] = &[ 318 | TestCase { 319 | points: ((1.0, 2.0), (3.0, 5.0)), 320 | angle: 0.9827989414909313, 321 | }, 322 | TestCase { 323 | points: ((1.0, 2.0), (3.0, -5.0)), 324 | angle: 4.990698112028704, 325 | }, 326 | TestCase { 327 | points: ((1.0, 2.0), (-3.0, 5.0)), 328 | angle: 2.4980739417195164, 329 | }, 330 | TestCase { 331 | points: ((1.0, 2.0), (-3.0, -5.0)), 332 | angle: 4.19326091952823, 333 | }, 334 | ]; 335 | for &TestCase { points, angle } in TEST_CASES { 336 | let (p1, p2) = points; 337 | let actual_angle = super::angle(p1, p2); 338 | if angle != actual_angle { 339 | panic!( 340 | "angle({:?} ~> {:?}): got {}, want {}", 341 | p1, p2, actual_angle, angle 342 | ); 343 | } 344 | } 345 | } 346 | 347 | #[test] 348 | fn test_add_polar_offset() { 349 | assert_eq!( 350 | add_polar_offset((10.0, 20.0), PI / 6.0, 1.0), 351 | (10.865494166666666, 20.499669166666667), 352 | ); 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/rand.rs: -------------------------------------------------------------------------------- 1 | use std::num::Wrapping; 2 | 3 | // Linear congruential generator parameters 4 | const MUL: u64 = 6364136223846793005; // Knuth section 3.3.4 (p.108) 5 | const INC: u64 = 1442695040888963407; 6 | 7 | #[derive(Clone, PartialEq)] 8 | pub struct Rng { 9 | state: u64, 10 | next_gaussian: Option, 11 | } 12 | 13 | impl Rng { 14 | pub fn from_seed(seed: &[u8]) -> Rng { 15 | // NOTE(wchargin): There are endianness dragons here. The original JavaScript code uses 16 | // `DataView.setUint32` to portably write a big-endian integer into an `ArrayBuffer`. 17 | // However, this buffer is the backing storage of a `Uint16Array`, which later reads from 18 | // the data in platform-dependent order. 19 | // 20 | // This Rust port chooses to use the little-endian behavior everywhere for portability. The 21 | // original behavior can be recovered by changing `swap_bytes` to `to_be`. 22 | let lower = murmur2(seed, 1690382925).swap_bytes(); 23 | let upper = murmur2(seed, 72970470).swap_bytes(); 24 | let state = u64::from(lower) | (u64::from(upper) << 32); 25 | Rng { 26 | state, 27 | next_gaussian: None, 28 | } 29 | } 30 | 31 | /// Picks a random value uniformly distributed between `0.0` (inclusive) and `1.0` (exclusive). 32 | pub fn rnd(&mut self) -> f64 { 33 | let old_state = self.state; 34 | // Advance internal state. 35 | self.state = old_state.wrapping_mul(MUL).wrapping_add(INC); 36 | // Calculate output function (XSH RR) using the old state. 37 | // This is a standard PCG-XSH-RR generator (O'Neill 2014, section 6.3.1) but drops 3 bits 38 | // during the xorshift to be compatible with an anomaly in the JavaScript implementation. 39 | let xorshifted = ((((old_state >> 18) & !(3 << 30)) ^ old_state) >> 27) as u32; 40 | let fac = xorshifted.rotate_right((old_state >> 59) as u32); 41 | 2.0f64.powi(-32) * f64::from(fac) 42 | } 43 | 44 | /// Picks a random value uniformly distributed between `min` (inclusive) and `max` (exclusive). 45 | pub fn uniform(&mut self, min: f64, max: f64) -> f64 { 46 | self.rnd() * (max - min) + min 47 | } 48 | 49 | /// Picks a random value according to a Gaussian (normal) distribution with the given mean and 50 | /// standard deviation. 51 | /// 52 | /// # Implementation-defined approximation behavior 53 | /// 54 | /// The canonical QQL JavaScript algorithm uses `Math.sqrt` and `Math.log` here, which both 55 | /// have implementation-defined approximation behavior per ECMA-262. The values returned by 56 | /// [`f64::ln`] may differ slightly, and testing on my machine shows that the results do differ 57 | /// for about 7% of values selected uniformly at random by `Math.random()`. 58 | /// 59 | /// Therefore, the results of this `gauss` method may also differ slightly across the two 60 | /// implementations. For example: 61 | /// 62 | /// ```rust 63 | /// use qql::rand::Rng; 64 | /// let mut rng = Rng::from_seed(b"\x08"); 65 | /// assert_eq!(rng.gauss(0.0, 1.0), 1.0637608855800318); 66 | /// ``` 67 | /// 68 | /// ```javascript 69 | /// > import("./src/art/safe-random.js").then((r) => console.log(r.makeSeededRng("0x08").gauss())) 70 | /// 1.063760885580032 71 | /// ``` 72 | /// 73 | /// Here, the two normal deviates differ by one unit in the last place. 74 | pub fn gauss(&mut self, mean: f64, stdev: f64) -> f64 { 75 | if let Some(z) = self.next_gaussian.take() { 76 | return mean + stdev * z; 77 | } 78 | let (v1, v2, s) = loop { 79 | let v1 = self.rnd() * 2.0 - 1.0; 80 | let v2 = self.rnd() * 2.0 - 1.0; 81 | let s = v1 * v1 + v2 * v2; 82 | if s < 1.0 && s != 0.0 { 83 | break (v1, v2, s); 84 | } 85 | }; 86 | let multiplier = (-2.0 * f64::ln(s) / s).sqrt(); 87 | let (z1, z2) = (v1 * multiplier, v2 * multiplier); 88 | self.next_gaussian = Some(z2); 89 | mean + stdev * z1 90 | } 91 | 92 | /// Picks `true` with probability roughly `p`, or `false` otherwise. 93 | /// 94 | /// # Approximate correctness 95 | /// 96 | /// The probability that `true` is returned is actually roughly `p + 2^-32`. In particular, 97 | /// `true` may be returned even if `p` is exactly zero. 98 | /// 99 | /// ``` 100 | /// use qql::rand::Rng; 101 | /// let mut rng = Rng::from_seed(b"\x2e\x7e\x19\x00"); 102 | /// let odds: [bool; 76] = std::array::from_fn(|_| rng.odds(0.0)); 103 | /// assert_eq!(&odds[71..], &[false, false, false, false, true]); 104 | /// ``` 105 | pub fn odds(&mut self, p: f64) -> bool { 106 | self.rnd() <= p 107 | } 108 | 109 | /// Chooses an item from `items` at a uniformly random index. 110 | /// 111 | /// # Panics 112 | /// 113 | /// Panics if `items.is_empty()`. 114 | pub fn choice<'a, T>(&mut self, items: &'a [T]) -> &'a T { 115 | items 116 | .get(self.uniform(0.0, items.len() as f64) as usize) 117 | .expect("no items") 118 | } 119 | 120 | /// Given a slice of `(item, weight)` pairs, chooses an `item` with probability proportional to 121 | /// its `weight`. (The name `wc` is short for *weighted choice*.) 122 | /// 123 | /// Weights can be `u32`, `f64`, or any other type that can be [converted into 124 | /// `f64`][std::convert::Into]. 125 | /// 126 | /// # Panics 127 | /// 128 | /// Panics if `weighted_items.is_empty()`. 129 | pub fn wc<'a, T, Weight: Into + Copy>( 130 | &mut self, 131 | weighted_items: &'a [(T, Weight)], 132 | ) -> &'a T { 133 | let sum_weight: f64 = weighted_items.iter().map(|(_, w)| (*w).into()).sum(); 134 | let bisection = sum_weight * self.rnd(); 135 | 136 | let mut cum_weight: f64 = 0.0; 137 | for (value, weight) in weighted_items { 138 | cum_weight += (*weight).into(); 139 | if cum_weight >= bisection { 140 | return value; 141 | } 142 | } 143 | &weighted_items.last().expect("no items").0 144 | } 145 | 146 | /// Constructs a new vector with a uniformly random permutation of the elements in `xs`. 147 | /// 148 | /// # Caveats 149 | /// 150 | /// If the next `n` [uniform deviates][Rng::rnd] from the current state would contain any 151 | /// duplicates (where `n` is the number of elements in `xs`), the sort order is 152 | /// implementation-defined. The chance of this happening (if the internal state is chosen 153 | /// uniformly at random) is `n * (n - 1) / (2 * 2^32)`. 154 | pub fn shuffle>(&mut self, xs: I) -> Vec { 155 | let mut result: Vec<(f64, T)> = xs.into_iter().map(|x| (self.rnd(), x)).collect(); 156 | // Using an unstable sort here under the assumption that no keys collide. If any keys do 157 | // collide, then the callback defined in the JavaScript implementation is not a *consistent 158 | // comparator* (per ECMAScript spec section 23.1.3.30.1) and so the sort order is 159 | // implementation-defined, anyway. 160 | result.sort_unstable_by(|(k1, _), (k2, _)| k1.total_cmp(k2)); 161 | result.into_iter().map(|(_, x)| x).collect() 162 | } 163 | 164 | /// Mutates `xs` in place to "winnow it down" to contain at most `num` elements, preserving the 165 | /// original order. 166 | /// 167 | /// # Performance 168 | /// 169 | /// The current implementation takes time linear in the product of `xs.len()` and the number of 170 | /// elements removed. 171 | pub fn winnow(&mut self, xs: &mut Vec, num: usize) { 172 | // Inefficient quadratic implementation, but this function is only called twice per QQL on 173 | // a fairly small sequence, so we just go with it to be entropy-consistent with the 174 | // JavaScript implementation. 175 | while xs.len() > num { 176 | let index = (self.rnd() * (xs.len() as f64)) as usize; 177 | xs.remove(index); 178 | } 179 | } 180 | } 181 | 182 | #[cfg(test)] 183 | mod test { 184 | use super::*; 185 | use hex_literal::hex; 186 | 187 | #[test] 188 | fn test_seed_state() { 189 | assert_eq!(Rng::from_seed(b"").state, 0x381a85e943aeeb00); 190 | assert_eq!( 191 | Rng::from_seed(&hex!( 192 | "efa7bdd92b5e9cd9de9b54ac0e3dc60623f1c989a80ed9c5157fffff10c2a148" 193 | )) 194 | .state, 195 | 0x506997572177a894 196 | ); 197 | } 198 | 199 | #[test] 200 | fn test_rnd_sequence() { 201 | let mut rng = Rng::from_seed(b""); 202 | let us: [f64; 8] = std::array::from_fn(|_| rng.rnd()); 203 | assert_eq!( 204 | us, 205 | [ 206 | 0.8438512671273202, 207 | 0.43491613143123686, 208 | 0.26782758394256234, 209 | 0.9794597257860005, 210 | 0.8957886048592627, 211 | 0.5943453973159194, 212 | 0.07430003909394145, 213 | 0.37728449678979814 214 | ] 215 | ); 216 | 217 | let mut rng = Rng::from_seed(&hex!( 218 | "efa7bdd92b5e9cd9de9b54ac0e3dc60623f1c989a80ed9c5157fffff10c2a148" 219 | )); 220 | let us: [f64; 8] = std::array::from_fn(|_| rng.rnd()); 221 | assert_eq!( 222 | us, 223 | [ 224 | 0.40630031237378716, 225 | 0.590646798722446, 226 | 0.5958091835491359, 227 | 0.09100268967449665, 228 | 0.9242822963278741, 229 | 0.808205850655213, 230 | 0.7671284528914839, 231 | 0.9752047171350569 232 | ] 233 | ); 234 | } 235 | 236 | #[test] 237 | fn test_uniform_sequence() { 238 | let mut rng = Rng::from_seed(b""); 239 | let vs: [f64; 8] = std::array::from_fn(|i| rng.uniform(i as f64, i as f64 * 2.0 + 3.0)); 240 | assert_eq!( 241 | vs, 242 | [ 243 | 2.5315538013819605, 244 | 2.7396645257249475, 245 | 3.3391379197128117, 246 | 8.876758354716003, 247 | 10.270520234014839, 248 | 9.754763178527355, 249 | 6.668700351845473, 250 | 10.772844967897981 251 | ] 252 | ); 253 | } 254 | 255 | #[test] 256 | fn test_gauss_sequence() { 257 | let mut rng = Rng::from_seed(b""); 258 | // Grab more samples than usual to get better coverage on the rejection sampling. 259 | let vs: [f64; 32] = std::array::from_fn(|_| rng.gauss(10.0, 2.0)); 260 | assert_eq!( 261 | vs, 262 | [ 263 | 12.347622663830158, 264 | 9.555644025458262, 265 | 11.766414589742986, 266 | 10.421065902979189, 267 | 8.663257202843637, 268 | 9.614660370965233, 269 | 12.005698572460942, 270 | 9.628512069963776, 271 | 8.916242878757988, 272 | 12.876768026032124, 273 | 10.617258413596735, 274 | 14.006192320114256, 275 | 9.947034706000881, 276 | 10.230187477724286, 277 | 7.451754035760429, 278 | 11.148342846306345, 279 | 9.390119721601897, 280 | 9.944130446086874, 281 | 7.709356813603907, 282 | 10.325955650684277, 283 | 8.378478731186833, 284 | 7.097538510395173, 285 | 10.939522022890161, 286 | 11.26899183993857, 287 | 9.026276357070047, 288 | 7.307428436569156, 289 | 10.764942658443658, 290 | 9.065278355076405, 291 | 6.629640618523688, 292 | 12.26010079693567, 293 | 6.424702181087971, 294 | 10.32136095339319 295 | ] 296 | ); 297 | } 298 | 299 | #[test] 300 | fn test_gauss_commutes_when_cached() { 301 | let mut rng = Rng::from_seed(b""); 302 | rng.gauss(0.0, 0.0); 303 | let y1 = rng.gauss(0.0, 0.0); 304 | 305 | let mut rng = Rng::from_seed(b""); 306 | rng.gauss(0.0, 0.0); 307 | rng.rnd(); 308 | rng.rnd(); 309 | rng.rnd(); 310 | let y2 = rng.gauss(0.0, 0.0); 311 | 312 | assert_eq!(y1, y2); 313 | } 314 | 315 | #[test] 316 | fn test_odds_sequence() { 317 | let mut rng = Rng::from_seed(b""); 318 | let vs: [bool; 16] = std::array::from_fn(|i| rng.odds(i as f64 / 16.0)); 319 | assert_eq!( 320 | vs, 321 | [ 322 | false, false, false, false, false, false, true, true, // 0..8 323 | false, true, false, false, true, true, true, true 324 | ] 325 | ); 326 | } 327 | 328 | #[test] 329 | fn test_choice_sequence() { 330 | let mut rng = Rng::from_seed(b""); 331 | 332 | let colors = &["red", "green", "blue"]; 333 | let fingers = &[1, 2, 3, 4, 5]; 334 | 335 | let colors_vs: [&str; 8] = std::array::from_fn(|_| *rng.choice(colors)); 336 | let fingers_vs: [u32; 8] = std::array::from_fn(|_| *rng.choice(fingers)); 337 | 338 | assert_eq!( 339 | colors_vs, 340 | ["blue", "green", "red", "blue", "blue", "green", "red", "green"] 341 | ); 342 | assert_eq!(fingers_vs, [5, 3, 5, 5, 3, 4, 3, 4]); 343 | } 344 | 345 | #[test] 346 | fn test_wc_distribution() { 347 | use std::collections::HashMap; 348 | let mut rng = Rng::from_seed(b""); 349 | let weighted_items = &[("red", 1), ("green", 3), ("blue", 2)]; 350 | let mut counts = HashMap::new(); 351 | for _ in 0..10000 { 352 | let color = *rng.wc(weighted_items); 353 | *counts.entry(color).or_default() += 1; 354 | } 355 | assert_eq!( 356 | counts, 357 | HashMap::from([("red", 1692), ("green", 4983), ("blue", 3325)]) 358 | ); 359 | } 360 | 361 | #[test] 362 | fn test_wc_with_float_weights_sequence() { 363 | let mut rng = Rng::from_seed(b""); 364 | 365 | let choices = &[("red", 1.3), ("green", 3.0), ("blue", 3.0)]; 366 | let values: [&str; 20] = std::array::from_fn(|_| *rng.wc(choices)); 367 | assert_eq!( 368 | values, 369 | [ 370 | "blue", "green", "green", "blue", "blue", "blue", "red", "green", "blue", "green", 371 | "blue", "blue", "green", "blue", "green", "blue", "green", "blue", "blue", "red" 372 | ] 373 | ); 374 | } 375 | 376 | #[test] 377 | fn test_shuffle_empty() { 378 | let mut rng = Rng::from_seed(b""); 379 | assert_eq!(rng.shuffle(Vec::<()>::new()), Vec::<()>::new()); 380 | } 381 | 382 | #[test] 383 | fn test_shuffle_singleton() { 384 | let mut rng = Rng::from_seed(b""); 385 | assert_eq!(rng.shuffle(vec![777]), vec![777]); 386 | assert_eq!(rng.shuffle(vec![777]), vec![777]); 387 | assert_eq!(rng.shuffle(vec![777]), vec![777]); 388 | } 389 | 390 | #[test] 391 | fn test_shuffle_sequence() { 392 | let mut rng = Rng::from_seed(b""); 393 | 394 | let colors = vec!['r', 'o', 'y', 'g', 'b', 'i', 'v']; 395 | assert_eq!( 396 | rng.shuffle(colors.clone()), 397 | vec!['v', 'y', 'o', 'i', 'r', 'b', 'g'] 398 | ); 399 | assert_eq!( 400 | rng.shuffle(colors.clone()), 401 | vec!['r', 'i', 'y', 'v', 'o', 'b', 'g'] 402 | ); 403 | assert_eq!( 404 | rng.shuffle(colors.clone()), 405 | vec!['i', 'v', 'y', 'r', 'o', 'b', 'g'] 406 | ); 407 | } 408 | 409 | #[test] 410 | fn test_winnow_sequence() { 411 | let mut rng = Rng::from_seed(b""); 412 | 413 | let mut colors = vec!['r', 'o', 'y', 'g', 'b', 'i', 'v']; 414 | rng.winnow(&mut colors, 999); 415 | assert_eq!(colors, vec!['r', 'o', 'y', 'g', 'b', 'i', 'v']); 416 | rng.winnow(&mut colors, 4); 417 | assert_eq!(colors, vec!['r', 'g', 'b', 'v']); 418 | rng.winnow(&mut colors, 4); 419 | assert_eq!(colors, vec!['r', 'g', 'b', 'v']); 420 | rng.winnow(&mut colors, 1); 421 | assert_eq!(colors, vec!['r']); 422 | 423 | colors = vec!['r', 'o', 'y', 'g', 'b', 'i', 'v']; 424 | rng.winnow(&mut colors, 4); 425 | assert_eq!(colors, vec!['o', 'y', 'b', 'i']); 426 | rng.winnow(&mut colors, 1); 427 | assert_eq!(colors, vec!['o']); 428 | } 429 | } 430 | 431 | fn murmur2(bytes: &[u8], seed: u32) -> u32 { 432 | const K: usize = 16; 433 | const MASK: Wrapping = Wrapping(0xffff); 434 | const MASK_BYTE: Wrapping = Wrapping(0xff); 435 | const M: Wrapping = Wrapping(0x5bd1e995); 436 | 437 | let mut l: usize = bytes.len(); 438 | let mut h = Wrapping(seed ^ (l as u32)); 439 | let mut i = 0; 440 | 441 | let byte32 = |i: usize| Wrapping(u32::from(bytes[i])); 442 | 443 | while l >= 4 { 444 | let mut k = (byte32(i) & MASK_BYTE) 445 | | ((byte32(i + 1) & MASK_BYTE) << 8) 446 | | ((byte32(i + 2) & MASK_BYTE) << 16) 447 | | ((byte32(i + 3) & MASK_BYTE) << 24); 448 | i += 4; 449 | k = (k & MASK) * M + ((((k >> K) * M) & MASK) << K); 450 | k ^= k >> 24; 451 | k = (k & MASK) * M + ((((k >> K) * M) & MASK) << K); 452 | h = ((h & MASK) * M + ((((h >> K) * M) & MASK) << K)) ^ k; 453 | l -= 4; 454 | } 455 | if l >= 3 { 456 | h ^= (byte32(i + 2) & MASK_BYTE) << K; 457 | } 458 | if l >= 2 { 459 | h ^= (byte32(i + 1) & MASK_BYTE) << 8; 460 | } 461 | if l >= 1 { 462 | h ^= byte32(i) & MASK_BYTE; 463 | h = (h & MASK) * M + ((((h >> K) * M) & MASK) << K); 464 | } 465 | 466 | h ^= h >> 13; 467 | h = (h & MASK) * M + ((((h >> K) * M) & MASK) << K); 468 | h ^= h >> 15; 469 | 470 | h.0 471 | } 472 | 473 | #[cfg(test)] 474 | mod murmur2_test { 475 | use super::*; 476 | use hex_literal::hex; 477 | 478 | #[test] 479 | fn test() { 480 | assert_eq!(murmur2(b"", 0), 0); 481 | assert_eq!(murmur2(b"\x12", 0), 0x85701953); 482 | assert_eq!(murmur2(b"\x12\x34", 0), 0xb106ed81); 483 | assert_eq!(murmur2(b"\x12\x34\x56", 0), 0xb21b79ab); 484 | assert_eq!(murmur2(b"\x12\x34\x56\x78", 0), 0x52bcf091); 485 | 486 | let bytes = &hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); 487 | assert_eq!(murmur2(bytes, 0x64c1324d), 0x142b44e9); 488 | assert_eq!(murmur2(bytes, 0x045970e6), 0x788be436); 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qqlrs 2 | 3 | Alternative renderer for [QQL][] artwork. 4 | 5 | This is a from-scratch re-implementation of the QQL algorithm, ported fairly 6 | directly from the original JavaScript source to Rust by [@wchargin][]. Some 7 | notable characteristics: 8 | 9 | - **Compatibility:** This implementation is entropy-compatible with the 10 | original, using the same RNG algorithms and sampling. A given QQL seed 11 | should look the same in both implementations (see ["Fidelity 12 | expectations"][fidelity] for details). 13 | 14 | - **Performance:** Between a faster core (sequential) implementation 15 | and support for **multicore rendering** of a single QQL, this 16 | renderer often runs 4–8× faster than the original. 17 | 18 | - **Features:** This implementation has some additional knobs and dials, for 19 | speed, quality, flexibility, and fun. See ["Features"][features] below. 20 | 21 | - **Hackability:** This code is not obfuscated or minified. Structures and 22 | dataflow are explicit. There's no hidden or global state. Some components 23 | have unit tests. Try poking around: read it, play with it, dump 24 | intermediate data structures, write custom searchers, override variables, 25 | tweak it yourself. (Just don't suggest that outputs from a tweaked 26 | algorithm are "official" QQLs, please.) 27 | 28 | As a quick, imperfect performance comparison, rendering some QQLs at 29 | 9600×12000px on my laptop, across the original JavaScript renderer, this 30 | implementation in sequential mode, and this implementation in parallel 31 | mode (`--chunks 2x4`): 32 | 33 | | Piece | Original | Sequential | Parallel | Speedup | 34 | |---|--:|--:|--:|--:| 35 | | [QQL #237][qql237] | 31.57s | 9.84s | 5.86s | **5.4× faster** | 36 | | [QQL #200][qql200] | 53.12s | 22.88s | 8.04s | **6.6× faster** | 37 | | [QQL #198][qql198] | 48.97s | 18.26s | 6.02s | **8.1× faster** | 38 | 39 | In addition to the speed improvements, this implementation tends to use 40 | about half as much RAM compared to the original. 41 | 42 | [QQL]: https://qql.art 43 | [@wchargin]: https://wchargin.com 44 | 45 | [qql237]: https://qql.art/token/0xda3c325ab45b30aeb476b026fe6a777443ca04f38e435bf353adffff10d0430d 46 | [qql200]: https://qql.art/token/0x83d1f3ef47a87d8d33fc4f73cbde3fd4f8f940fe23ae48e68f6bffff10620256 47 | [qql198]: https://qql.art/token/0x83d1f3ef47a87d8d33fc4f73cbde3fd4f8f940fe518da2975d6affff10c1a316 48 | 49 | ## Attribution 50 | 51 | The QQL project and the original art algorithm were created by Tyler Hobbs and 52 | Dandelion Indigo Lee. This implementation was written by @wchargin. 53 | 54 | This repository is dual-licensed under Apache 2.0 and MIT terms. 55 | 56 | ## Getting started 57 | 58 | This project uses the standard [Rust][] toolchain. You will need Rust 1.67.0 or 59 | newer. To build: 60 | 61 | ```shell 62 | $ cargo test --release 63 | $ cargo build --release 64 | ``` 65 | 66 | This creates a binary `target/release/qql-cli`, which you can invoke. In its 67 | most basic form, simply pass a seed that you want to render: 68 | 69 | ```shell 70 | $ ./target/release/qql-cli 0x33c9371d25ce44a408f8a6473fbad86bf81e1a178c012cd49a85ffff14c54b46 71 | ``` 72 | 73 | A close-up of QQL #85, showing the face of the creature. 74 | 75 | Pass any additional options after the seed. For example, to render a 76 | high-resolution version of just the "face" of [QQL #85][qql085], with 8 threads 77 | of parallelism, and save it to a temporary file: 78 | 79 | ```shell 80 | ./target/release/qql-cli \ 81 | 0x7d265f38b1d92b48997620b050cf0c534e1908fc203f633b2894ffff10e10c55 \ 82 | --viewport 0.242x0.380+0.378+0.521 \ 83 | --width 19200 \ 84 | --min-circle-steps 32 \ 85 | --chunks 2x4 \ 86 | -o /tmp/out.png \ 87 | ; 88 | ``` 89 | 90 | Despite rendering a 42 megapixel view into a 461 megapixel virtual canvas, this 91 | completes in just 1.7 seconds on my laptop, using 399 MB of RAM at peak. 92 | 93 | To learn about what options are available, pass `-h` to read help text, or keep 94 | reading this document. 95 | 96 |
97 | 98 | As a shortcut to build and run together, you can use: 99 | 100 | ``` 101 | $ cargo run --release -- [...] 102 | ``` 103 | 104 | (Don't forget the extra two hyphens after `--release`.) 105 | 106 | [Rust]: https://www.rust-lang.org/ 107 | [qql085]: https://qql.art/token/0x7d265f38b1d92b48997620b050cf0c534e1908fc203f633b2894ffff10e10c55 108 | 109 | ## Features 110 | [features]: #features 111 | 112 | *Jump:* 113 | 114 | - [Viewport restriction, `--viewport`](#viewport-restriction) 115 | - [Multicore rendering, `--chunks`](#multicore-rendering) 116 | - [Incremental animations, `--animate`](#incremental-animations) 117 | - [Higher quality circles, `--min-circle-steps`](#higher-quality-circles) 118 | - [Fast collision checking, `--fast-collisions`](#fast-collision-checking) 119 | - [Radius inflation at paint time, `--inflate-draw-radius`](#radius-inflation-at-paint-time) 120 | 121 | ### Viewport restriction 122 | 123 | > **TL;DR:** Use the `--viewport ` option to render just part of the 124 | > canvas, where each input ranges from 0.0 to 1.0. 125 | 126 | If you want just a section of a QQL, the obvious idea is to render the full 127 | thing and then crop it. But there's a much faster way, which this 128 | implementation provides: restrict the viewport while rendering, so that we 129 | don't have to paint all the circles just to throw most of them away. 130 | 131 | 132 | A single messy circle taken from the center of a QQL that is 160 thousand times larger than this viewport. This image rendered in 2 seconds. 133 | 134 | With this technique, you can easily blow up small sections of QQLs to huge 135 | resolutions, even if rendering the full image at that resolution would be 136 | practically impossible. It's easy to zoom into a QQL far enough that the full 137 | image would have trillions of pixels, consume terabytes of RAM, and take days 138 | or longer to render. Yet a small window into this virtual canvas can be 139 | rendered in under a second. 140 | 141 | Use the **`--viewport `** option to specify what section of the canvas 142 | you want to render, where each component is a number between 0.0 and 1.0 143 | specifying the width, height, x-offset (from left), and y-offset (from top) of 144 | the viewport, respectively. For example, `--viewport 0.1x0.2+0.3+0.4` specifies 145 | a viewport that is 10% as wide and 20% as tall as the original canvas, with its 146 | top-left corner corner 30% of the way in from the left and 40% of the way down 147 | from the top. Its bottom-right corner is thus 40% of the way in from the right 148 | and 60% of the way down from the top. 149 | 150 | Note that when using the `--viewport` option, the `--width` still corresponds 151 | to the *virtual canvas* width. So, for instance, a QQL with `--width 10000` 152 | will be 10000px wide and 12500px tall. If you add `--viewport 0.1x0.1+0+0` to 153 | render just the top-left corner, the resulting image will be 1000px wide and 154 | 1250px tall. 155 | 156 |
157 | 158 | A convenient way to visually find your viewport coordinates is to render a 159 | small version of your seed, import it into an image editor like GIMP, and 160 | rescale it to 1000×1000px (which will distort the aspect ratio; that's okay). 161 | Then, use the crop tool to draw a rectangle over the viewport that you want to 162 | render, and just read off the position and size from the side menus. Divide 163 | by 1000 to get the relative numbers. For example, with [QQL #237][qql237]: 164 | 165 | ![Screenshot of GIMP with a QQL resized to 1000×1000 and an active crop window, 166 | whose numeric coordinates are highlighted.][qql237-how-to-viewport] 167 | 168 | (If you prefer not to distort the aspect ratio, you can resize to 1000×1250px 169 | instead. Just divide the vertical coordinates by 1250 instead of 1000.) 170 | 171 | [qql237-how-to-viewport]: docs/img/qql237_how_to_viewport.png 172 | 173 | ### Multicore rendering 174 | 175 | > **TL;DR:** If your CPU has (say) 8 threads, add `--chunks 2x4` to render most 176 | > QQLs faster. 177 | 178 | Any headless renderer makes it easy to render *many* QQLs in parallel: simply 179 | start multiple copies of the renderer, tasked with different seeds. But this 180 | implementation also lets you take advantage of parallelism when rendering a 181 | *single* QQL at high resolution. 182 | 183 | How does it work? First, we compute layout information just once. Then, we 184 | divide the canvas into a grid: say, 2 cells wide by 4 cells high, to take 185 | advantage of 8 threads. Each thread performs a viewport-restricted render of 186 | just its grid. Once all the threads are done, we assemble their results onto 187 | the final canvas. 188 | 189 | The main advantage is that each thread gets to skip over any circles that are 190 | entirely outside its grid cell. If there are many circles in the QQL that touch 191 | all the grid cells, then this will be less effective. However, even when few or 192 | no circles are able to be culled, we can still see significant speedups, which 193 | may be due to cache locality: each thread has a smaller amount of image data to 194 | work with. Even using a `2x1` chunk layout on [QQL #26][qql026], where every 195 | circle needs to be painted in every chunk, I still see a 1.3× speedup! For QQLs 196 | that *are* better suited due to having circles well distributed, speedups on 197 | the order of 2–3× are more common. 198 | 199 | Use the **`--chunks `** option to specify a grid layout. For example, 200 | `--chunks 2x4` uses a grid 2 chunks wide and 4 chunks high. Ideally, the 201 | product of `W` and `H` should be about as many threads as your CPU has. You can 202 | exceed that number (like, `--chunks 16x16`) but this will probably make things 203 | slower. The default value is `--chunks 1x1`, which corresponds to no 204 | parallelism. 205 | 206 | In principle, you might see some seams or artifacts along the chunk boundaries. 207 | In practice, these tend to be entirely imperceptible, even with the aid of 208 | digital analysis. If you find otherwise, let me know! I'd love to take a look. 209 | 210 | [qql026]: https://qql.art/token/0x01f93c96448ebd097a1a2a5358a4b3550ad0354c5e0fbf8464caffff11280ac8 211 | 212 | ### Incremental animations 213 | 214 | > **TL;DR:** Pass `--animate points:100` to render a sequence of images as each 215 | > 100 points are added, or `--animate groups` to render a sequence of images as 216 | > each flow line group is added. 217 | 218 | By default, rendering a QQL will create just one image: the final QQL. By 219 | passing the **`--animate `** flag, you can instead create a *sequence* 220 | of images that show the process of the QQL be drawn, circle by circle! The very 221 | first image will be blank, and each image after that will have more and more 222 | circles, until the last image, which is the complete QQL. 223 | 224 | If you pass **`--animate points:N`**, where `N` is a positive integer (e.g., 225 | `--animate points:100`), each image will have up to `N` points more than the 226 | previous. If you pass **`--animate groups`**, each image will have one 227 | additional *flow line group*, which is a collection of points that were created 228 | together in the algorithm and have similar properties. Both modes, and 229 | especially `--animate groups`, can help you understand how the algorithm works. 230 | 231 | Note that some points may be just barely off screen, or may have very small (or 232 | negative) radii. Therefore, some frames may be identical to their predecessors. 233 | 234 | This example shows the flow line groups of QQL #53: 235 | 236 |

237 | Incremental render of QQL #53, showing one group at a time. 238 |

239 | 240 | When rendering for animation, the interpretation of the output path parameter 241 | (`-o`) changes a bit: frame numbers are added to each image before the file 242 | extension, zero-padded to 4 places. So, if you pass `-o build/out.png`, images 243 | will be created at `build/out0000.png`, `build/out0001.png`, and so on. 244 | 245 | If you want to convert this image sequence to a video file for sharing, you can 246 | use [FFmpeg][]. For example, if you rendered `-o build/out.png` and want to 247 | convert that to an MP4 video `build/out.mp4`, you can run: 248 | 249 | ``` 250 | ffmpeg -framerate 30 -i build/out%04d.png -vf format=yuv420p build/out.mp4 251 | ``` 252 | 253 | (The `-vf format=yuv420p` arguments instruct FFmpeg to use YUV 4:2:0 chroma 254 | subsampling; this slightly degrades color quality, but makes the video 255 | compatible with more players.) 256 | 257 | Rendering with `--animate` is *much faster* than successively re-rendering 258 | QQLs. All the layout information only has to be computed once, and each frame 259 | only needs to incrementally render the new points. Most of the time goes to 260 | actually writing PNG files to disk. Exact speed of course depends on a lot of 261 | factors, but I typically see around **30 frames per second** on my laptop. 262 | 263 | You can also explicitly pass **`--animate none`**, which is the same as the 264 | default behavior. 265 | 266 | [ffmpeg]: https://ffmpeg.org 267 | 268 | ### Higher quality circles 269 | 270 | > **TL;DR:** If your `--width` is greater than 10000 or so, and you can see the 271 | > straight lines in your circles, try adding `--min-circle-steps 64`. 272 | 273 | In QQL, circles are approximated by polygons. The polygons for large circles 274 | have many segments, and the polygons for small circles have fewer segments (but 275 | never fewer than 8). At typical zoom levels, these just look like circles, but 276 | if you render at a really high resolution (say, 10k pixels wide at 100% zoom), 277 | you can start to see the individual line segments. 278 | 279 | Use the **`--min-circle-steps `** option to increase the minimum number 280 | of segments. You should increase this approximately linearly with `--width`. 281 | 282 | For example, here are two renderings of the "epicenter" of [QQL #24][qql024] 283 | with a virtual canvas width of 40000px\*. The first rendering leaves 284 | `--min-circle-steps` at its default value, while the second sets it to `64`. 285 | Note that the circles are visibly smoother in the second: 286 | 287 | | default | `--min-circle-steps 64` | 288 | |-----------------|-------------------------| 289 | | ![][qql024-ms8] | ![][qql024-ms64] | 290 | 291 | If you can't spot the difference immediately, this [visual diff][qql024-diff] 292 | may help you orient. 293 | 294 | \* Specifically, this shows a viewport of `0.030x0.030+0.650+0.651`. 295 | 296 | [qql024]: https://qql.art/token/0xeec815e73af687baba9a7cd213add4b5e05b316effad4ad36c86ffff126204cd 297 | [qql024-ms8]: docs/img/qql024_epicenter_minsteps_8.png 298 | [qql024-ms64]: docs/img/qql024_epicenter_minsteps_64.png 299 | [qql024-diff]: docs/img/qql024_epicenter_minsteps_diff.png 300 | 301 | ### Fast collision checking 302 | 303 | > **TL;DR:** The `--fast-collisions` option may provide a moderate performance 304 | > boost; it may also change the output. 305 | 306 | Passing the **`--fast-collisions`** option changes the way that the algorithm 307 | checks whether two circles would collide. In fact, not only does it become 308 | faster, it also becomes more accurate! This sounds like a good thing, but it 309 | does mean that the answer to "do these circles collide?" can differ from the 310 | original algorithm, which can change the layout of the whole piece. 311 | 312 | ### Radius inflation at paint time 313 | 314 | > **TL;DR:** The `--inflate-draw-radius` option dramatically changes the 315 | > appearance of some QQLs that have lots of negative space. 316 | 317 | When the QQL algorithm creates points along a flow field, each point gets a 318 | different radius. Sometimes a point gets a "negative" radius! These points 319 | still participate in layout, but don't get drawn at paint time, so QQLs with 320 | lots of negative-radius points tend to have… well, negative space. 321 | 322 | The **`--inflate-draw-radius`** option keeps the layout step as is, but, at 323 | paint time, makes sure that each point has at least a small positive radius. 324 | This brings all the negative-radius points to life! Renderings affected by this 325 | can look "fuzzy" due to all the small points. Here's an example, using a [seed 326 | from the marketplace][mp-negspace]: 327 | 328 | ![Two versions of a QQL. At left: the QQL has a swirl in the top-right with a 329 | curious smattering of points around the rest of the canvas, but mostly empty. 330 | At right: all the negative space is filled in by colorful, silky tendrils 331 | moving along the flow field.][mp-negspace-img] 332 | 333 | [mp-negspace]: https://qql.art/token/0xacfa3e822961cede6dabbbeef96838fec3ad3d47fb829b196094ffff10e06ab4 334 | [mp-negspace-img]: docs/img/marketplace_negspace_example.png 335 | 336 | > **Important:** Feel free to play around with this option, but if you share 337 | > images with others, please make sure to communicate that these are not 338 | > actually canonical QQLs! 339 | 340 | ## Fidelity expectations 341 | [fidelity]: #fidelity-expectations 342 | 343 | The output of this renderer is interpreted to match the canonical outputs "as 344 | well as the human eye can see at typical resolutions". This loosely defined 345 | goal is more lax than something like "bitwise identical to the pixel level", 346 | and there are some known deviations. 347 | 348 | ### Canvas details 349 | 350 | Both this implementation and the original use the `stroke` primitive on their 351 | respective canvas APIs to do all the actual painting. The implementations 352 | should emit "semantically the same" strokes, but the underlying canvas APIs may 353 | rasterize those strokes slightly differently. For example, you may see 354 | differences in antialiasing. 355 | 356 | This purely affects painting, not layout, so should never change the overall 357 | appearance of a QQL. These kinds of differences tend to be visible only at very 358 | high resolutions (say, 10k pixels wide at 100% zoom) and when directly 359 | comparing the outputs of the two implementations. 360 | 361 | ### Implementation-defined transcendentals 362 | 363 | *See docs on `qql::rand::gauss` for more details.* 364 | 365 | The original implementation uses `Math.log` as part of its Marsaglia algorithm 366 | for generating normal deviates. But this is defined in the ECMAScript spec as 367 | returning "an *implementation-approximated* Number value representing the 368 | result of the natural logarithm" (emphasis mine). The approximations may differ 369 | across implementations, so the original QQL implementation is not perfectly 370 | portable. 371 | 372 | This implementation also uses the analogous `f64::ln` from the Rust standard 373 | library, which similarly has platform-dependent behavior. On my system, the 374 | results of `f64::ln` are consistent with the standard `ln` functions in C, 375 | Java, Python, Ruby, Haskell, …but not with `Math.log` in Firefox or Chrome's 376 | JavaScript implementations. The differences are very slight, on the order of 377 | 10^−16, and so this is unlikely to cause a visible change in the algorithm. 378 | However, if such a logarithm happened to cross a discontinuity—e.g., causing 379 | two points to just barely collide instead of just barely not collide—then this 380 | could in principle affect layout and cause visible large-scale differences. 381 | 382 | (Similar for `Math.sqrt`, though implementations may be more consistent there.) 383 | 384 | Since this behavior is implementation-defined to begin with, I don't consider 385 | such deviations to be bugs, but if you run into one I'd love to hear about it 386 | anyway, just for kicks. 387 | 388 | ### Nondeterministic sorts 389 | 390 | *See docs on `qql::rand::shuffle` for more details.* 391 | 392 | The original implementation internally shuffles lists by pairing each element 393 | with a uniform deviate and sorting the pairs by the deviates. However, it 394 | doesn't account for the possibility that two deviates might collide, which 395 | happens with non-negigible probability since the uniform deviates are sampled 396 | from a space of size 2^32. 397 | 398 | When two such deviates collide, the sort order under the original algorithm is 399 | implementation-defined, because the callback is not a "consistent comparator". 400 | In this case, the canonical QQL algorithm behaves differently in Chrome and 401 | Firefox, so there's really no "right answer". This algorithm will do "something 402 | reasonable" (i.e., produce *a* valid permutation of the input that was to be 403 | shuffled) but it may or may not correspond with either Chrome or Firefox's 404 | behavior. 405 | 406 | As above, since this behavior is implementation-defined to begin with, I don't 407 | consider such deviations to be bugs, but if you run into one I'd love to hear 408 | about it anyway, just for kicks. 409 | 410 | ### Intentional tweaks 411 | 412 | Options like `--fast-collisions` and `--inflate-draw-radius` intentionally 413 | alter the behavior of the algorithm, in subtle or obvious ways. When these 414 | options are enabled, there may be significant deviations from the canonical 415 | algorithm; this is expected. 416 | 417 | ### Bugs 418 | 419 | If you're running into a layout or painting bug, and your QQL looks notably 420 | different when rendered under this implementation compared to the canonical 421 | one, let me know! Please send me the seed and the width that you're rendering 422 | it at, as well as a description of where in the image the difference is. 423 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "anstream" 13 | version = "0.3.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "9e579a7752471abc2a8268df8b20005e3eadd975f585398f17efcfd8d4927371" 16 | dependencies = [ 17 | "anstyle", 18 | "anstyle-parse", 19 | "anstyle-query", 20 | "anstyle-wincon", 21 | "colorchoice", 22 | "is-terminal", 23 | "utf8parse", 24 | ] 25 | 26 | [[package]] 27 | name = "anstyle" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 31 | 32 | [[package]] 33 | name = "anstyle-parse" 34 | version = "0.2.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 37 | dependencies = [ 38 | "utf8parse", 39 | ] 40 | 41 | [[package]] 42 | name = "anstyle-query" 43 | version = "1.0.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 46 | dependencies = [ 47 | "windows-sys", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-wincon" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "4bcd8291a340dd8ac70e18878bc4501dd7b4ff970cfa21c207d36ece51ea88fd" 55 | dependencies = [ 56 | "anstyle", 57 | "windows-sys", 58 | ] 59 | 60 | [[package]] 61 | name = "anyhow" 62 | version = "1.0.70" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" 65 | 66 | [[package]] 67 | name = "arrayvec" 68 | version = "0.5.2" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 71 | 72 | [[package]] 73 | name = "autocfg" 74 | version = "1.1.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 77 | 78 | [[package]] 79 | name = "bitflags" 80 | version = "1.3.2" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 83 | 84 | [[package]] 85 | name = "bytemuck" 86 | version = "1.14.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" 89 | 90 | [[package]] 91 | name = "byteorder" 92 | version = "1.4.3" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 95 | 96 | [[package]] 97 | name = "cc" 98 | version = "1.0.79" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 101 | 102 | [[package]] 103 | name = "cfg-if" 104 | version = "1.0.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 107 | 108 | [[package]] 109 | name = "clap" 110 | version = "4.2.4" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "956ac1f6381d8d82ab4684768f89c0ea3afe66925ceadb4eeb3fc452ffc55d62" 113 | dependencies = [ 114 | "clap_builder", 115 | "clap_derive", 116 | "once_cell", 117 | ] 118 | 119 | [[package]] 120 | name = "clap_builder" 121 | version = "4.2.4" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749" 124 | dependencies = [ 125 | "anstream", 126 | "anstyle", 127 | "bitflags", 128 | "clap_lex", 129 | "strsim", 130 | ] 131 | 132 | [[package]] 133 | name = "clap_derive" 134 | version = "4.2.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" 137 | dependencies = [ 138 | "heck", 139 | "proc-macro2", 140 | "quote", 141 | "syn", 142 | ] 143 | 144 | [[package]] 145 | name = "clap_lex" 146 | version = "0.4.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" 149 | 150 | [[package]] 151 | name = "cmake" 152 | version = "0.1.50" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" 155 | dependencies = [ 156 | "cc", 157 | ] 158 | 159 | [[package]] 160 | name = "color_quant" 161 | version = "1.1.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 164 | 165 | [[package]] 166 | name = "colorchoice" 167 | version = "1.0.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 170 | 171 | [[package]] 172 | name = "const-cstr" 173 | version = "0.3.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" 176 | 177 | [[package]] 178 | name = "core-foundation" 179 | version = "0.9.3" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 182 | dependencies = [ 183 | "core-foundation-sys", 184 | "libc", 185 | ] 186 | 187 | [[package]] 188 | name = "core-foundation-sys" 189 | version = "0.8.4" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 192 | 193 | [[package]] 194 | name = "core-graphics" 195 | version = "0.22.3" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" 198 | dependencies = [ 199 | "bitflags", 200 | "core-foundation", 201 | "core-graphics-types", 202 | "foreign-types", 203 | "libc", 204 | ] 205 | 206 | [[package]] 207 | name = "core-graphics-types" 208 | version = "0.1.1" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" 211 | dependencies = [ 212 | "bitflags", 213 | "core-foundation", 214 | "foreign-types", 215 | "libc", 216 | ] 217 | 218 | [[package]] 219 | name = "core-text" 220 | version = "19.2.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" 223 | dependencies = [ 224 | "core-foundation", 225 | "core-graphics", 226 | "foreign-types", 227 | "libc", 228 | ] 229 | 230 | [[package]] 231 | name = "crc32fast" 232 | version = "1.3.2" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 235 | dependencies = [ 236 | "cfg-if", 237 | ] 238 | 239 | [[package]] 240 | name = "dirs-next" 241 | version = "2.0.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 244 | dependencies = [ 245 | "cfg-if", 246 | "dirs-sys-next", 247 | ] 248 | 249 | [[package]] 250 | name = "dirs-sys-next" 251 | version = "0.1.2" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 254 | dependencies = [ 255 | "libc", 256 | "redox_users", 257 | "winapi", 258 | ] 259 | 260 | [[package]] 261 | name = "dlib" 262 | version = "0.5.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" 265 | dependencies = [ 266 | "libloading", 267 | ] 268 | 269 | [[package]] 270 | name = "dwrote" 271 | version = "0.11.0" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" 274 | dependencies = [ 275 | "lazy_static", 276 | "libc", 277 | "winapi", 278 | "wio", 279 | ] 280 | 281 | [[package]] 282 | name = "errno" 283 | version = "0.3.1" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 286 | dependencies = [ 287 | "errno-dragonfly", 288 | "libc", 289 | "windows-sys", 290 | ] 291 | 292 | [[package]] 293 | name = "errno-dragonfly" 294 | version = "0.1.2" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 297 | dependencies = [ 298 | "cc", 299 | "libc", 300 | ] 301 | 302 | [[package]] 303 | name = "euclid" 304 | version = "0.22.9" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" 307 | dependencies = [ 308 | "num-traits", 309 | ] 310 | 311 | [[package]] 312 | name = "fdeflate" 313 | version = "0.3.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" 316 | dependencies = [ 317 | "simd-adler32", 318 | ] 319 | 320 | [[package]] 321 | name = "flate2" 322 | version = "1.0.25" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 325 | dependencies = [ 326 | "crc32fast", 327 | "miniz_oxide 0.6.2", 328 | ] 329 | 330 | [[package]] 331 | name = "float-ord" 332 | version = "0.2.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" 335 | 336 | [[package]] 337 | name = "font-kit" 338 | version = "0.11.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "21fe28504d371085fae9ac7a3450f0b289ab71e07c8e57baa3fb68b9e57d6ce5" 341 | dependencies = [ 342 | "bitflags", 343 | "byteorder", 344 | "core-foundation", 345 | "core-graphics", 346 | "core-text", 347 | "dirs-next", 348 | "dwrote", 349 | "float-ord", 350 | "freetype", 351 | "lazy_static", 352 | "libc", 353 | "log", 354 | "pathfinder_geometry", 355 | "pathfinder_simd", 356 | "walkdir", 357 | "winapi", 358 | "yeslogic-fontconfig-sys", 359 | ] 360 | 361 | [[package]] 362 | name = "foreign-types" 363 | version = "0.3.2" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 366 | dependencies = [ 367 | "foreign-types-shared", 368 | ] 369 | 370 | [[package]] 371 | name = "foreign-types-shared" 372 | version = "0.1.1" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 375 | 376 | [[package]] 377 | name = "freetype" 378 | version = "0.7.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6" 381 | dependencies = [ 382 | "freetype-sys", 383 | "libc", 384 | ] 385 | 386 | [[package]] 387 | name = "freetype-sys" 388 | version = "0.13.1" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a" 391 | dependencies = [ 392 | "cmake", 393 | "libc", 394 | "pkg-config", 395 | ] 396 | 397 | [[package]] 398 | name = "getrandom" 399 | version = "0.2.9" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" 402 | dependencies = [ 403 | "cfg-if", 404 | "libc", 405 | "wasi", 406 | ] 407 | 408 | [[package]] 409 | name = "heck" 410 | version = "0.4.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 413 | 414 | [[package]] 415 | name = "hermit-abi" 416 | version = "0.3.1" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 419 | 420 | [[package]] 421 | name = "hex" 422 | version = "0.4.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 425 | 426 | [[package]] 427 | name = "hex-literal" 428 | version = "0.3.4" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" 431 | 432 | [[package]] 433 | name = "image" 434 | version = "0.24.7" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" 437 | dependencies = [ 438 | "bytemuck", 439 | "byteorder", 440 | "color_quant", 441 | "num-rational", 442 | "num-traits", 443 | "png", 444 | ] 445 | 446 | [[package]] 447 | name = "io-lifetimes" 448 | version = "1.0.10" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" 451 | dependencies = [ 452 | "hermit-abi", 453 | "libc", 454 | "windows-sys", 455 | ] 456 | 457 | [[package]] 458 | name = "is-terminal" 459 | version = "0.4.7" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 462 | dependencies = [ 463 | "hermit-abi", 464 | "io-lifetimes", 465 | "rustix", 466 | "windows-sys", 467 | ] 468 | 469 | [[package]] 470 | name = "itoa" 471 | version = "1.0.6" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 474 | 475 | [[package]] 476 | name = "lazy_static" 477 | version = "1.4.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 480 | 481 | [[package]] 482 | name = "libc" 483 | version = "0.2.142" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" 486 | 487 | [[package]] 488 | name = "libloading" 489 | version = "0.7.4" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" 492 | dependencies = [ 493 | "cfg-if", 494 | "winapi", 495 | ] 496 | 497 | [[package]] 498 | name = "linux-raw-sys" 499 | version = "0.3.4" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" 502 | 503 | [[package]] 504 | name = "log" 505 | version = "0.4.17" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 508 | dependencies = [ 509 | "cfg-if", 510 | ] 511 | 512 | [[package]] 513 | name = "lyon_geom" 514 | version = "0.17.7" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "71d89ccbdafd83d259403e22061be27bccc3254bba65cdc5303250c4227c8c8e" 517 | dependencies = [ 518 | "arrayvec", 519 | "euclid", 520 | "num-traits", 521 | ] 522 | 523 | [[package]] 524 | name = "miniz_oxide" 525 | version = "0.6.2" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 528 | dependencies = [ 529 | "adler", 530 | ] 531 | 532 | [[package]] 533 | name = "miniz_oxide" 534 | version = "0.7.1" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 537 | dependencies = [ 538 | "adler", 539 | "simd-adler32", 540 | ] 541 | 542 | [[package]] 543 | name = "num-integer" 544 | version = "0.1.45" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 547 | dependencies = [ 548 | "autocfg", 549 | "num-traits", 550 | ] 551 | 552 | [[package]] 553 | name = "num-rational" 554 | version = "0.4.1" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" 557 | dependencies = [ 558 | "autocfg", 559 | "num-integer", 560 | "num-traits", 561 | ] 562 | 563 | [[package]] 564 | name = "num-traits" 565 | version = "0.2.15" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 568 | dependencies = [ 569 | "autocfg", 570 | ] 571 | 572 | [[package]] 573 | name = "once_cell" 574 | version = "1.17.1" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 577 | 578 | [[package]] 579 | name = "pathfinder_geometry" 580 | version = "0.5.1" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" 583 | dependencies = [ 584 | "log", 585 | "pathfinder_simd", 586 | ] 587 | 588 | [[package]] 589 | name = "pathfinder_simd" 590 | version = "0.5.1" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff" 593 | dependencies = [ 594 | "rustc_version", 595 | ] 596 | 597 | [[package]] 598 | name = "pest" 599 | version = "2.5.7" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122" 602 | dependencies = [ 603 | "thiserror", 604 | "ucd-trie", 605 | ] 606 | 607 | [[package]] 608 | name = "pkg-config" 609 | version = "0.3.26" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 612 | 613 | [[package]] 614 | name = "png" 615 | version = "0.17.8" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa" 618 | dependencies = [ 619 | "bitflags", 620 | "crc32fast", 621 | "fdeflate", 622 | "flate2", 623 | "miniz_oxide 0.7.1", 624 | ] 625 | 626 | [[package]] 627 | name = "proc-macro2" 628 | version = "1.0.56" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" 631 | dependencies = [ 632 | "unicode-ident", 633 | ] 634 | 635 | [[package]] 636 | name = "qql" 637 | version = "0.1.0" 638 | dependencies = [ 639 | "anyhow", 640 | "clap", 641 | "hex", 642 | "hex-literal", 643 | "image", 644 | "raqote", 645 | "serde", 646 | "serde_json", 647 | ] 648 | 649 | [[package]] 650 | name = "quote" 651 | version = "1.0.26" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 654 | dependencies = [ 655 | "proc-macro2", 656 | ] 657 | 658 | [[package]] 659 | name = "raqote" 660 | version = "0.8.2" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "f5b6cb89f8be6a645e5834f3ad44a7960d12343d97b5b7fb07cb0365ae36aa2d" 663 | dependencies = [ 664 | "euclid", 665 | "font-kit", 666 | "lyon_geom", 667 | "pathfinder_geometry", 668 | "png", 669 | "sw-composite", 670 | "typed-arena", 671 | ] 672 | 673 | [[package]] 674 | name = "redox_syscall" 675 | version = "0.2.16" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 678 | dependencies = [ 679 | "bitflags", 680 | ] 681 | 682 | [[package]] 683 | name = "redox_users" 684 | version = "0.4.3" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 687 | dependencies = [ 688 | "getrandom", 689 | "redox_syscall", 690 | "thiserror", 691 | ] 692 | 693 | [[package]] 694 | name = "rustc_version" 695 | version = "0.3.3" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" 698 | dependencies = [ 699 | "semver", 700 | ] 701 | 702 | [[package]] 703 | name = "rustix" 704 | version = "0.37.14" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "d9b864d3c18a5785a05953adeed93e2dca37ed30f18e69bba9f30079d51f363f" 707 | dependencies = [ 708 | "bitflags", 709 | "errno", 710 | "io-lifetimes", 711 | "libc", 712 | "linux-raw-sys", 713 | "windows-sys", 714 | ] 715 | 716 | [[package]] 717 | name = "ryu" 718 | version = "1.0.13" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 721 | 722 | [[package]] 723 | name = "same-file" 724 | version = "1.0.6" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 727 | dependencies = [ 728 | "winapi-util", 729 | ] 730 | 731 | [[package]] 732 | name = "semver" 733 | version = "0.11.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" 736 | dependencies = [ 737 | "semver-parser", 738 | ] 739 | 740 | [[package]] 741 | name = "semver-parser" 742 | version = "0.10.2" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" 745 | dependencies = [ 746 | "pest", 747 | ] 748 | 749 | [[package]] 750 | name = "serde" 751 | version = "1.0.160" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" 754 | dependencies = [ 755 | "serde_derive", 756 | ] 757 | 758 | [[package]] 759 | name = "serde_derive" 760 | version = "1.0.160" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" 763 | dependencies = [ 764 | "proc-macro2", 765 | "quote", 766 | "syn", 767 | ] 768 | 769 | [[package]] 770 | name = "serde_json" 771 | version = "1.0.96" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" 774 | dependencies = [ 775 | "itoa", 776 | "ryu", 777 | "serde", 778 | ] 779 | 780 | [[package]] 781 | name = "simd-adler32" 782 | version = "0.3.5" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" 785 | 786 | [[package]] 787 | name = "strsim" 788 | version = "0.10.0" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 791 | 792 | [[package]] 793 | name = "sw-composite" 794 | version = "0.7.16" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "9ac8fb7895b4afa060ad731a32860db8755da3449a47e796d5ecf758db2671d4" 797 | 798 | [[package]] 799 | name = "syn" 800 | version = "2.0.15" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" 803 | dependencies = [ 804 | "proc-macro2", 805 | "quote", 806 | "unicode-ident", 807 | ] 808 | 809 | [[package]] 810 | name = "thiserror" 811 | version = "1.0.40" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 814 | dependencies = [ 815 | "thiserror-impl", 816 | ] 817 | 818 | [[package]] 819 | name = "thiserror-impl" 820 | version = "1.0.40" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 823 | dependencies = [ 824 | "proc-macro2", 825 | "quote", 826 | "syn", 827 | ] 828 | 829 | [[package]] 830 | name = "typed-arena" 831 | version = "2.0.2" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" 834 | 835 | [[package]] 836 | name = "ucd-trie" 837 | version = "0.1.5" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" 840 | 841 | [[package]] 842 | name = "unicode-ident" 843 | version = "1.0.8" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 846 | 847 | [[package]] 848 | name = "utf8parse" 849 | version = "0.2.1" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 852 | 853 | [[package]] 854 | name = "walkdir" 855 | version = "2.3.3" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" 858 | dependencies = [ 859 | "same-file", 860 | "winapi-util", 861 | ] 862 | 863 | [[package]] 864 | name = "wasi" 865 | version = "0.11.0+wasi-snapshot-preview1" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 868 | 869 | [[package]] 870 | name = "winapi" 871 | version = "0.3.9" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 874 | dependencies = [ 875 | "winapi-i686-pc-windows-gnu", 876 | "winapi-x86_64-pc-windows-gnu", 877 | ] 878 | 879 | [[package]] 880 | name = "winapi-i686-pc-windows-gnu" 881 | version = "0.4.0" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 884 | 885 | [[package]] 886 | name = "winapi-util" 887 | version = "0.1.5" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 890 | dependencies = [ 891 | "winapi", 892 | ] 893 | 894 | [[package]] 895 | name = "winapi-x86_64-pc-windows-gnu" 896 | version = "0.4.0" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 899 | 900 | [[package]] 901 | name = "windows-sys" 902 | version = "0.48.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 905 | dependencies = [ 906 | "windows-targets", 907 | ] 908 | 909 | [[package]] 910 | name = "windows-targets" 911 | version = "0.48.0" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 914 | dependencies = [ 915 | "windows_aarch64_gnullvm", 916 | "windows_aarch64_msvc", 917 | "windows_i686_gnu", 918 | "windows_i686_msvc", 919 | "windows_x86_64_gnu", 920 | "windows_x86_64_gnullvm", 921 | "windows_x86_64_msvc", 922 | ] 923 | 924 | [[package]] 925 | name = "windows_aarch64_gnullvm" 926 | version = "0.48.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 929 | 930 | [[package]] 931 | name = "windows_aarch64_msvc" 932 | version = "0.48.0" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 935 | 936 | [[package]] 937 | name = "windows_i686_gnu" 938 | version = "0.48.0" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 941 | 942 | [[package]] 943 | name = "windows_i686_msvc" 944 | version = "0.48.0" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 947 | 948 | [[package]] 949 | name = "windows_x86_64_gnu" 950 | version = "0.48.0" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 953 | 954 | [[package]] 955 | name = "windows_x86_64_gnullvm" 956 | version = "0.48.0" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 959 | 960 | [[package]] 961 | name = "windows_x86_64_msvc" 962 | version = "0.48.0" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 965 | 966 | [[package]] 967 | name = "wio" 968 | version = "0.2.2" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" 971 | dependencies = [ 972 | "winapi", 973 | ] 974 | 975 | [[package]] 976 | name = "yeslogic-fontconfig-sys" 977 | version = "3.2.0" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386" 980 | dependencies = [ 981 | "const-cstr", 982 | "dlib", 983 | "once_cell", 984 | "pkg-config", 985 | ] 986 | -------------------------------------------------------------------------------- /src/colordata.json: -------------------------------------------------------------------------------- 1 | {"colors":[{"name":"Austin Blue","hue":205,"hueMin":203,"hueMax":207,"hueVariance":1,"sat":85,"satMin":83,"satMax":87,"satVariance":1,"bright":70,"brightMin":68,"brightMax":72,"brightVariance":1,"alpha":100},{"name":"Austin Green","hue":175,"hueMin":173,"hueMax":177,"hueVariance":1,"sat":70,"satMin":68,"satMax":72,"satVariance":1,"bright":60,"brightMin":58,"brightMax":62,"brightVariance":1,"alpha":100},{"name":"Austin White","hue":40,"hueMin":38,"hueMax":42,"hueVariance":1,"sat":4,"satMin":2,"satMax":6,"satVariance":1,"bright":88,"brightMin":86,"brightMax":90,"brightVariance":1,"alpha":100},{"name":"Austin Yellow","hue":45,"hueMin":43,"hueMax":47,"hueVariance":1,"sat":50,"satMin":48,"satMax":52,"satVariance":1,"bright":99,"brightMin":97,"brightMax":100,"brightVariance":1,"alpha":100},{"name":"Berlin Cool Purple","hue":255,"hueMin":253,"hueMax":257,"hueVariance":1,"sat":60,"satMin":58,"satMax":62,"satVariance":1,"bright":17,"brightMin":15,"brightMax":19,"brightVariance":1,"alpha":100},{"name":"Berlin Dark Green","hue":175,"hueMin":173,"hueMax":177,"hueVariance":1,"sat":52,"satMin":50,"satMax":54,"satVariance":1,"bright":8,"brightMin":6,"brightMax":10,"brightVariance":1,"alpha":100},{"name":"Berlin Gray Blue","hue":225,"hueMin":223,"hueMax":227,"hueVariance":1,"sat":50,"satMin":48,"satMax":52,"satVariance":1,"bright":8,"brightMin":6,"brightMax":10,"brightVariance":1,"alpha":100},{"name":"Berlin Intense Blue","hue":230,"hueMin":228,"hueMax":232,"hueVariance":1,"sat":95,"satMin":93,"satMax":97,"satVariance":1,"bright":15,"brightMin":13,"brightMax":17,"brightVariance":1,"alpha":100},{"name":"Berlin Warm Purple","hue":330,"hueMin":328,"hueMax":332,"hueVariance":1,"sat":63,"satMin":61,"satMax":65,"satVariance":1,"bright":8,"brightMin":6,"brightMax":10,"brightVariance":1,"alpha":100},{"name":"Edinburgh Blue","hue":208,"hueMin":206,"hueMax":210,"hueVariance":1,"sat":52,"satMin":50,"satMax":54,"satVariance":1,"bright":18,"brightMin":16,"brightMax":20,"brightVariance":1,"alpha":100},{"name":"Edinburgh Dark Brown","hue":28,"hueMin":26,"hueMax":30,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":18,"brightMin":16,"brightMax":20,"brightVariance":1,"alpha":100},{"name":"Edinburgh Green","hue":150,"hueMin":148,"hueMax":152,"hueVariance":1,"sat":34,"satMin":32,"satMax":36,"satVariance":1,"bright":20,"brightMin":18,"brightMax":22,"brightVariance":1,"alpha":100},{"name":"Edinburgh Mid Brown","hue":30,"hueMin":28,"hueMax":32,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":1,"bright":50,"brightMin":48,"brightMax":52,"brightVariance":1,"alpha":100},{"name":"Edinburgh Tan","hue":35,"hueMin":33,"hueMax":37,"hueVariance":1,"sat":25,"satMin":23,"satMax":27,"satVariance":1,"bright":82,"brightMin":80,"brightMax":84,"brightVariance":1,"alpha":100},{"name":"Fidenza Brown","hue":25,"hueMin":23,"hueMax":27,"hueVariance":1,"sat":43,"satMin":41,"satMax":45,"satVariance":1,"bright":18,"brightMin":16,"brightMax":20,"brightVariance":1,"alpha":100},{"name":"Fidenza Dark Blue","hue":225,"hueMin":223,"hueMax":227,"hueVariance":1,"sat":60,"satMin":58,"satMax":62,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"Fidenza Green","hue":170,"hueMin":168,"hueMax":172,"hueVariance":1,"sat":56,"satMin":54,"satMax":58,"satVariance":1,"bright":62,"brightMin":60,"brightMax":64,"brightVariance":1,"alpha":100},{"name":"Fidenza Light Green","hue":160,"hueMin":158,"hueMax":162,"hueVariance":1,"sat":10,"satMin":8,"satMax":12,"satVariance":1,"bright":80,"brightMin":78,"brightMax":82,"brightVariance":1,"alpha":100},{"name":"Fidenza Newsprint","hue":40,"hueMin":38,"hueMax":42,"hueVariance":1,"sat":8,"satMin":6,"satMax":10,"satVariance":1,"bright":92,"brightMin":90,"brightMax":94,"brightVariance":1,"alpha":100},{"name":"Fidenza Pink","hue":12,"hueMin":10,"hueMax":14,"hueVariance":1,"sat":13,"satMin":11,"satMax":15,"satVariance":1,"bright":93,"brightMin":91,"brightMax":95,"brightVariance":1,"alpha":100},{"name":"Miami Dark Purple","hue":250,"hueMin":248,"hueMax":252,"hueVariance":1,"sat":50,"satMin":48,"satMax":52,"satVariance":1,"bright":35,"brightMin":33,"brightMax":37,"brightVariance":1,"alpha":100},{"name":"Miami Rich Blue","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":75,"satMin":73,"satMax":77,"satVariance":1,"bright":75,"brightMin":73,"brightMax":77,"brightVariance":1,"alpha":100},{"name":"Miami White","hue":218,"hueMin":216,"hueMax":220,"hueVariance":1,"sat":0,"satMin":0,"satMax":2,"satVariance":1,"bright":88,"brightMin":86,"brightMax":90,"brightVariance":1,"alpha":100},{"name":"Seattle Dark Gray","hue":80,"hueMin":78,"hueMax":82,"hueVariance":1,"sat":15,"satMin":13,"satMax":17,"satVariance":1,"bright":10,"brightMin":8,"brightMax":12,"brightVariance":1,"alpha":100},{"name":"Seattle Mid Blue","hue":205,"hueMin":203,"hueMax":207,"hueVariance":1,"sat":35,"satMin":33,"satMax":37,"satVariance":1,"bright":58,"brightMin":56,"brightMax":60,"brightVariance":1,"alpha":100},{"name":"Seattle Warm Gray","hue":45,"hueMin":43,"hueMax":47,"hueVariance":1,"sat":12,"satMin":10,"satMax":14,"satVariance":1,"bright":80,"brightMin":78,"brightMax":82,"brightVariance":1,"alpha":100},{"name":"Seoul Dark Blue","hue":218,"hueMin":216,"hueMax":220,"hueVariance":1,"sat":70,"satMin":68,"satMax":72,"satVariance":1,"bright":30,"brightMin":28,"brightMax":32,"brightVariance":1,"alpha":100},{"name":"Seoul Dark Gray","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":42,"satMin":40,"satMax":44,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"Seoul Light Blue","hue":210,"hueMin":208,"hueMax":212,"hueVariance":1,"sat":41,"satMin":39,"satMax":43,"satVariance":1,"bright":75,"brightMin":73,"brightMax":77,"brightVariance":1,"alpha":100},{"name":"Seoul Light Gray","hue":40,"hueMin":38,"hueMax":42,"hueVariance":1,"sat":1,"satMin":0,"satMax":3,"satVariance":1,"bright":80,"brightMin":78,"brightMax":82,"brightVariance":1,"alpha":100},{"name":"Seoul Light Pink","hue":0,"hueMin":-2,"hueMax":2,"hueVariance":1,"sat":15,"satMin":13,"satMax":17,"satVariance":1,"bright":84,"brightMin":82,"brightMax":86,"brightVariance":1,"alpha":100},{"name":"Seoul Mid Blue","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":1,"bright":58,"brightMin":56,"brightMax":60,"brightVariance":1,"alpha":100},{"name":"Seoul Mid Pink","hue":0,"hueMin":-2,"hueMax":2,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":80,"brightMin":78,"brightMax":82,"brightVariance":1,"alpha":100},{"name":"aAquamarine","hue":185,"hueMin":183,"hueMax":187,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":1,"bright":60,"brightMin":58,"brightMax":62,"brightVariance":1,"alpha":100},{"name":"aAquamarine3","hue":180,"hueMin":178,"hueMax":182,"hueVariance":1,"sat":67,"satMin":65,"satMax":69,"satVariance":1,"bright":62,"brightMin":60,"brightMax":64,"brightVariance":1,"alpha":100},{"name":"aDarkBlue","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":78,"satMin":76,"satMax":80,"satVariance":1,"bright":50,"brightMin":48,"brightMax":52,"brightVariance":1,"alpha":100},{"name":"aDarkBrown","hue":27,"hueMin":25,"hueMax":29,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":30,"brightMin":28,"brightMax":32,"brightVariance":1,"alpha":100},{"name":"aDarkBrown3","hue":35,"hueMin":33,"hueMax":37,"hueVariance":1,"sat":20,"satMin":18,"satMax":22,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"aIndigo","hue":225,"hueMin":223,"hueMax":227,"hueVariance":1,"sat":60,"satMin":58,"satMax":62,"satVariance":1,"bright":45,"brightMin":43,"brightMax":47,"brightVariance":1,"alpha":100},{"name":"aIndigo2","hue":222,"hueMin":220,"hueMax":224,"hueVariance":1,"sat":65,"satMin":63,"satMax":67,"satVariance":1,"bright":42,"brightMin":40,"brightMax":44,"brightVariance":1,"alpha":100},{"name":"aLightGreen","hue":165,"hueMin":163,"hueMax":167,"hueVariance":1,"sat":14,"satMin":12,"satMax":16,"satVariance":1,"bright":77,"brightMin":75,"brightMax":79,"brightVariance":1,"alpha":100},{"name":"aMidBlue","hue":205,"hueMin":203,"hueMax":207,"hueVariance":1,"sat":85,"satMin":83,"satMax":87,"satVariance":1,"bright":70,"brightMin":68,"brightMax":72,"brightVariance":1,"alpha":100},{"name":"aOrange","hue":18,"hueMin":16,"hueMax":20,"hueVariance":0,"sat":85,"satMin":83,"satMax":87,"satVariance":1,"bright":88,"brightMin":86,"brightMax":90,"brightVariance":0.5,"alpha":100},{"name":"aOrange3","hue":15,"hueMin":13,"hueMax":17,"hueVariance":0,"sat":75,"satMin":73,"satMax":77,"satVariance":1,"bright":85,"brightMin":83,"brightMax":87,"brightVariance":0.5,"alpha":100},{"name":"aOrange4","hue":15,"hueMin":13,"hueMax":17,"hueVariance":0,"sat":70,"satMin":68,"satMax":72,"satVariance":1,"bright":81,"brightMin":79,"brightMax":83,"brightVariance":0.5,"alpha":100},{"name":"aPeach","hue":16,"hueMin":14,"hueMax":18,"hueVariance":1,"sat":32,"satMin":30,"satMax":34,"satVariance":1,"bright":92,"brightMin":90,"brightMax":94,"brightVariance":1,"alpha":100},{"name":"aPink","hue":353,"hueMin":351,"hueMax":355,"hueVariance":1,"sat":27,"satMin":25,"satMax":29,"satVariance":1,"bright":87,"brightMin":85,"brightMax":89,"brightVariance":1,"alpha":100},{"name":"aYellow","hue":45,"hueMin":43,"hueMax":47,"hueVariance":0,"sat":80,"satMin":78,"satMax":82,"satVariance":1,"bright":94,"brightMin":92,"brightMax":96,"brightVariance":0.5,"alpha":100},{"name":"aYellow3","hue":47,"hueMin":45,"hueMax":49,"hueVariance":0,"sat":73,"satMin":71,"satMax":75,"satVariance":1,"bright":91,"brightMin":89,"brightMax":93,"brightVariance":0.5,"alpha":100},{"name":"bCoolDarkGreen","hue":175,"hueMin":173,"hueMax":177,"hueVariance":1,"sat":52,"satMin":50,"satMax":54,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"bCoolDarkGreen2","hue":175,"hueMin":173,"hueMax":177,"hueVariance":1,"sat":60,"satMin":58,"satMax":62,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"bCoolMidGreen","hue":170,"hueMin":168,"hueMax":172,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":40,"brightMin":38,"brightMax":42,"brightVariance":1,"alpha":100},{"name":"bDarkRed","hue":0,"hueMin":-2,"hueMax":2,"hueVariance":1,"sat":78,"satMin":76,"satMax":80,"satVariance":1,"bright":46,"brightMin":44,"brightMax":48,"brightVariance":1,"alpha":100},{"name":"bDimOrange","hue":30,"hueMin":28,"hueMax":32,"hueVariance":1,"sat":83,"satMin":81,"satMax":85,"satVariance":1,"bright":90,"brightMin":88,"brightMax":92,"brightVariance":1,"alpha":100},{"name":"bGrayBlue","hue":210,"hueMin":208,"hueMax":212,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":40,"brightMin":38,"brightMax":42,"brightVariance":1,"alpha":100},{"name":"bGrayBlue2","hue":220,"hueMin":218,"hueMax":222,"hueVariance":1,"sat":52,"satMin":50,"satMax":54,"satVariance":1,"bright":50,"brightMin":48,"brightMax":52,"brightVariance":1,"alpha":100},{"name":"bGrayBlue3","hue":222,"hueMin":220,"hueMax":224,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":1,"bright":50,"brightMin":48,"brightMax":52,"brightVariance":1,"alpha":100},{"name":"bGrayPurple","hue":345,"hueMin":343,"hueMax":347,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"bGrayPurple2","hue":340,"hueMin":338,"hueMax":342,"hueVariance":1,"sat":65,"satMin":63,"satMax":67,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"bGrayPurple3","hue":330,"hueMin":328,"hueMax":332,"hueVariance":1,"sat":52,"satMin":50,"satMax":54,"satVariance":1,"bright":17,"brightMin":15,"brightMax":19,"brightVariance":1,"alpha":100},{"name":"bGrayPurple4","hue":325,"hueMin":323,"hueMax":327,"hueVariance":1,"sat":70,"satMin":68,"satMax":72,"satVariance":1,"bright":20,"brightMin":18,"brightMax":22,"brightVariance":1,"alpha":100},{"name":"bIndigo","hue":232,"hueMin":230,"hueMax":234,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":1,"bright":27,"brightMin":25,"brightMax":29,"brightVariance":1,"alpha":100},{"name":"bIndigo2","hue":230,"hueMin":228,"hueMax":232,"hueVariance":1,"sat":60,"satMin":58,"satMax":62,"satVariance":1,"bright":27,"brightMin":25,"brightMax":29,"brightVariance":1,"alpha":100},{"name":"bIntenseBlue","hue":210,"hueMin":208,"hueMax":212,"hueVariance":1,"sat":60,"satMin":58,"satMax":62,"satVariance":1,"bright":35,"brightMin":33,"brightMax":37,"brightVariance":1,"alpha":100},{"name":"bIntenseBlue2","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":58,"satMin":56,"satMax":60,"satVariance":1,"bright":27,"brightMin":25,"brightMax":29,"brightVariance":1,"alpha":100},{"name":"bIntenseBlue3","hue":220,"hueMin":218,"hueMax":222,"hueVariance":1,"sat":70,"satMin":68,"satMax":72,"satVariance":1,"bright":35,"brightMin":33,"brightMax":37,"brightVariance":1,"alpha":100},{"name":"bIntenseBlue4","hue":225,"hueMin":223,"hueMax":227,"hueVariance":1,"sat":76,"satMin":74,"satMax":78,"satVariance":1,"bright":32,"brightMin":30,"brightMax":34,"brightVariance":1,"alpha":100},{"name":"bRed","hue":8,"hueMin":6,"hueMax":10,"hueVariance":1,"sat":88,"satMin":86,"satMax":90,"satVariance":1,"bright":65,"brightMin":63,"brightMax":67,"brightVariance":1,"alpha":100},{"name":"eBrown","hue":30,"hueMin":28,"hueMax":32,"hueVariance":1,"sat":35,"satMin":33,"satMax":37,"satVariance":1,"bright":35,"brightMin":33,"brightMax":37,"brightVariance":1,"alpha":100},{"name":"eBrown2","hue":25,"hueMin":23,"hueMax":27,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"eBrown3","hue":27,"hueMin":25,"hueMax":29,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":30,"brightMin":28,"brightMax":32,"brightVariance":1,"alpha":100},{"name":"eBrown4","hue":28,"hueMin":26,"hueMax":30,"hueVariance":1,"sat":37,"satMin":35,"satMax":39,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"eCoolDarkGreen","hue":170,"hueMin":168,"hueMax":172,"hueVariance":1,"sat":52,"satMin":50,"satMax":54,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"eCoolDarkGreen2","hue":165,"hueMin":163,"hueMax":167,"hueVariance":1,"sat":50,"satMin":48,"satMax":52,"satVariance":1,"bright":23,"brightMin":21,"brightMax":25,"brightVariance":1,"alpha":100},{"name":"eCream","hue":40,"hueMin":38,"hueMax":42,"hueVariance":1,"sat":4,"satMin":2,"satMax":6,"satVariance":0.5,"bright":96,"brightMin":94,"brightMax":98,"brightVariance":0.5,"alpha":100},{"name":"eCream2","hue":40,"hueMin":38,"hueMax":42,"hueVariance":1,"sat":12,"satMin":10,"satMax":14,"satVariance":0.5,"bright":93,"brightMin":91,"brightMax":95,"brightVariance":0.5,"alpha":100},{"name":"eGrayBlue","hue":200,"hueMin":198,"hueMax":202,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":40,"brightMin":38,"brightMax":42,"brightVariance":1,"alpha":100},{"name":"eGrayBlue2","hue":200,"hueMin":198,"hueMax":202,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":1,"bright":42,"brightMin":40,"brightMax":44,"brightVariance":1,"alpha":100},{"name":"eGrayBlue3","hue":205,"hueMin":203,"hueMax":207,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":1,"bright":42,"brightMin":40,"brightMax":44,"brightVariance":1,"alpha":100},{"name":"eMidGreen","hue":150,"hueMin":148,"hueMax":152,"hueVariance":1,"sat":60,"satMin":58,"satMax":62,"satVariance":1,"bright":35,"brightMin":33,"brightMax":37,"brightVariance":1,"alpha":100},{"name":"eOrange","hue":30,"hueMin":28,"hueMax":32,"hueVariance":0.5,"sat":89,"satMin":87,"satMax":91,"satVariance":0.5,"bright":94,"brightMin":92,"brightMax":96,"brightVariance":0.5,"alpha":100},{"name":"ePaleYellow","hue":41,"hueMin":39,"hueMax":43,"hueVariance":0.2,"sat":75,"satMin":73,"satMax":77,"satVariance":1,"bright":98,"brightMin":96,"brightMax":100,"brightVariance":0.2,"alpha":100},{"name":"ePaleYellow2","hue":42,"hueMin":40,"hueMax":44,"hueVariance":0.2,"sat":70,"satMin":68,"satMax":72,"satVariance":1,"bright":100,"brightMin":98,"brightMax":100,"brightVariance":0.2,"alpha":100},{"name":"ePink","hue":5,"hueMin":3,"hueMax":7,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":95,"brightMin":94,"brightMax":96,"brightVariance":0.5,"alpha":100},{"name":"ePink2","hue":5,"hueMin":3,"hueMax":7,"hueVariance":1,"sat":35,"satMin":33,"satMax":37,"satVariance":0.3,"bright":97,"brightMin":95,"brightMax":99,"brightVariance":0.3,"alpha":100},{"name":"eRed","hue":8,"hueMin":6,"hueMax":10,"hueVariance":1,"sat":88,"satMin":86,"satMax":90,"satVariance":1,"bright":87,"brightMin":85,"brightMax":89,"brightVariance":1,"alpha":100},{"name":"eRed2","hue":6,"hueMin":4,"hueMax":8,"hueVariance":1,"sat":84,"satMin":82,"satMax":86,"satVariance":1,"bright":84,"brightMin":82,"brightMax":86,"brightVariance":1,"alpha":100},{"name":"eRedOrange","hue":15,"hueMin":13,"hueMax":17,"hueVariance":1,"sat":88,"satMin":86,"satMax":90,"satVariance":1,"bright":87,"brightMin":85,"brightMax":89,"brightVariance":1,"alpha":100},{"name":"eYellow","hue":43,"hueMin":42,"hueMax":44,"hueVariance":0.2,"sat":90,"satMin":80,"satMax":94,"satVariance":2,"bright":97,"brightMin":96,"brightMax":98,"brightVariance":0.2,"alpha":100},{"name":"fBlue","hue":210,"hueMin":208,"hueMax":212,"hueVariance":1,"sat":65,"satMin":63,"satMax":67,"satVariance":1,"bright":55,"brightMin":53,"brightMax":57,"brightVariance":1,"alpha":100},{"name":"fBrown","hue":25,"hueMin":23,"hueMax":27,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":33,"brightMin":31,"brightMax":35,"brightVariance":1,"alpha":100},{"name":"fDarkBlue","hue":220,"hueMin":218,"hueMax":222,"hueVariance":1,"sat":65,"satMin":63,"satMax":67,"satVariance":1,"bright":35,"brightMin":33,"brightMax":37,"brightVariance":1,"alpha":100},{"name":"fDarkBrown","hue":25,"hueMin":23,"hueMax":27,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":23,"brightMin":21,"brightMax":25,"brightVariance":1,"alpha":100},{"name":"fDarkRed","hue":358,"hueMin":356,"hueMax":360,"hueVariance":1,"sat":64,"satMin":62,"satMax":66,"satVariance":1,"bright":86,"brightMin":84,"brightMax":88,"brightVariance":1,"alpha":100},{"name":"fExtraDarkBlue","hue":225,"hueMin":223,"hueMax":227,"hueVariance":1,"sat":65,"satMin":63,"satMax":67,"satVariance":1,"bright":20,"brightMin":18,"brightMax":22,"brightVariance":1,"alpha":100},{"name":"fExtraDarkBrown","hue":25,"hueMin":23,"hueMax":27,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":15,"brightMin":13,"brightMax":17,"brightVariance":1,"alpha":100},{"name":"fGreen","hue":170,"hueMin":168,"hueMax":172,"hueVariance":1,"sat":75,"satMin":73,"satMax":77,"satVariance":1,"bright":65,"brightMin":63,"brightMax":67,"brightVariance":1,"alpha":100},{"name":"fNewsprint","hue":40,"hueMin":38,"hueMax":42,"hueVariance":1,"sat":12,"satMin":10,"satMax":14,"satVariance":1,"bright":88,"brightMin":86,"brightMax":90,"brightVariance":1,"alpha":100},{"name":"fOrange","hue":25,"hueMin":23,"hueMax":27,"hueVariance":1,"sat":78,"satMin":76,"satMax":80,"satVariance":1,"bright":90,"brightMin":88,"brightMax":92,"brightVariance":1,"alpha":100},{"name":"fPaleBlue","hue":200,"hueMin":198,"hueMax":202,"hueVariance":1,"sat":35,"satMin":33,"satMax":37,"satVariance":1,"bright":75,"brightMin":73,"brightMax":77,"brightVariance":1,"alpha":100},{"name":"fPaleGreen","hue":160,"hueMin":158,"hueMax":162,"hueVariance":1,"sat":15,"satMin":13,"satMax":17,"satVariance":1,"bright":85,"brightMin":83,"brightMax":87,"brightVariance":1,"alpha":100},{"name":"fPaleYellow","hue":43,"hueMin":41,"hueMax":45,"hueVariance":1,"sat":60,"satMin":58,"satMax":62,"satVariance":1,"bright":99,"brightMin":97,"brightMax":100,"brightVariance":1,"alpha":100},{"name":"fPink","hue":11,"hueMin":9,"hueMax":13,"hueVariance":1,"sat":35,"satMin":33,"satMax":37,"satVariance":1,"bright":97,"brightMin":95,"brightMax":99,"brightVariance":1,"alpha":100},{"name":"fRed","hue":358,"hueMin":356,"hueMax":360,"hueVariance":1,"sat":80,"satMin":78,"satMax":82,"satVariance":1,"bright":82,"brightMin":80,"brightMax":84,"brightVariance":1,"alpha":100},{"name":"fYellow","hue":43,"hueMin":41,"hueMax":45,"hueVariance":1,"sat":90,"satMin":88,"satMax":92,"satVariance":1,"bright":99,"brightMin":97,"brightMax":100,"brightVariance":1,"alpha":100},{"name":"mBlue","hue":205,"hueMin":203,"hueMax":207,"hueVariance":1,"sat":64,"satMin":62,"satMax":66,"satVariance":1,"bright":85,"brightMin":83,"brightMax":87,"brightVariance":1,"alpha":100},{"name":"mBlue2","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":50,"satMin":48,"satMax":52,"satVariance":1,"bright":87,"brightMin":85,"brightMax":89,"brightVariance":1,"alpha":100},{"name":"mGreen","hue":181,"hueMin":179,"hueMax":183,"hueVariance":1,"sat":31,"satMin":29,"satMax":33,"satVariance":1,"bright":85,"brightMin":83,"brightMax":87,"brightVariance":1,"alpha":100},{"name":"mLightBlue","hue":200,"hueMin":198,"hueMax":202,"hueVariance":1,"sat":43,"satMin":41,"satMax":45,"satVariance":1,"bright":94,"brightMin":92,"brightMax":96,"brightVariance":1,"alpha":100},{"name":"mLightBlue2","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":43,"satMin":41,"satMax":45,"satVariance":1,"bright":94,"brightMin":92,"brightMax":96,"brightVariance":1,"alpha":100},{"name":"mOrange","hue":35,"hueMin":33,"hueMax":37,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":0,"bright":94,"brightMin":92,"brightMax":96,"brightVariance":0,"alpha":100},{"name":"mPink","hue":350,"hueMin":348,"hueMax":352,"hueVariance":1,"sat":22,"satMin":20,"satMax":24,"satVariance":1,"bright":95,"brightMin":93,"brightMax":97,"brightVariance":1,"alpha":100},{"name":"mPink2","hue":335,"hueMin":333,"hueMax":337,"hueVariance":1,"sat":14,"satMin":12,"satMax":16,"satVariance":1,"bright":95,"brightMin":93,"brightMax":97,"brightVariance":1,"alpha":100},{"name":"mPurple","hue":245,"hueMin":243,"hueMax":247,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":73,"brightMin":71,"brightMax":75,"brightVariance":1,"alpha":100},{"name":"mPurple2","hue":255,"hueMin":253,"hueMax":257,"hueVariance":1,"sat":46,"satMin":44,"satMax":48,"satVariance":1,"bright":53,"brightMin":51,"brightMax":55,"brightVariance":1,"alpha":100},{"name":"mTeal","hue":179,"hueMin":177,"hueMax":181,"hueVariance":1,"sat":38,"satMin":36,"satMax":40,"satVariance":1,"bright":75,"brightMin":73,"brightMax":77,"brightVariance":1,"alpha":100},{"name":"mWhite","hue":218,"hueMin":216,"hueMax":220,"hueVariance":1,"sat":0,"satMin":0,"satMax":2,"satVariance":1,"bright":88,"brightMin":86,"brightMax":90,"brightVariance":1,"alpha":100},{"name":"mYellow","hue":47,"hueMin":45,"hueMax":49,"hueVariance":0.2,"sat":65,"satMin":63,"satMax":67,"satVariance":0,"bright":95,"brightMin":93,"brightMax":97,"brightVariance":0.2,"alpha":100},{"name":"mYellow2","hue":44,"hueMin":42,"hueMax":46,"hueVariance":0.2,"sat":60,"satMin":58,"satMax":62,"satVariance":0,"bright":95,"brightMin":93,"brightMax":97,"brightVariance":0.2,"alpha":100},{"name":"sDarkBlue","hue":210,"hueMin":208,"hueMax":212,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":20,"brightMin":18,"brightMax":22,"brightVariance":1,"alpha":100},{"name":"sDarkBlue2","hue":200,"hueMin":198,"hueMax":202,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":20,"brightMin":18,"brightMax":22,"brightVariance":1,"alpha":100},{"name":"sDarkGrayBrown","hue":80,"hueMin":78,"hueMax":82,"hueVariance":1,"sat":15,"satMin":13,"satMax":17,"satVariance":1,"bright":15,"brightMin":13,"brightMax":17,"brightVariance":1,"alpha":100},{"name":"sDarkGrayBrown2","hue":80,"hueMin":78,"hueMax":82,"hueVariance":1,"sat":20,"satMin":18,"satMax":22,"satVariance":1,"bright":18,"brightMin":16,"brightMax":20,"brightVariance":1,"alpha":100},{"name":"sDarkGreen","hue":100,"hueMin":98,"hueMax":102,"hueVariance":1,"sat":30,"satMin":28,"satMax":32,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"sDarkMidGreen","hue":100,"hueMin":98,"hueMax":102,"hueVariance":1,"sat":30,"satMin":28,"satMax":32,"satVariance":1,"bright":30,"brightMin":28,"brightMax":32,"brightVariance":1,"alpha":100},{"name":"sLightBlue","hue":203,"hueMin":201,"hueMax":205,"hueVariance":1,"sat":38,"satMin":36,"satMax":40,"satVariance":1,"bright":62,"brightMin":60,"brightMax":64,"brightVariance":1,"alpha":100},{"name":"sLightBlue2","hue":203,"hueMin":201,"hueMax":205,"hueVariance":1,"sat":36,"satMin":34,"satMax":38,"satVariance":1,"bright":64,"brightMin":62,"brightMax":66,"brightVariance":1,"alpha":100},{"name":"sLightYellow","hue":41,"hueMin":39,"hueMax":43,"hueVariance":1,"sat":55,"satMin":53,"satMax":57,"satVariance":1,"bright":92,"brightMin":90,"brightMax":94,"brightVariance":1,"alpha":100},{"name":"sLightYellowGreen","hue":65,"hueMin":63,"hueMax":67,"hueVariance":1,"sat":17,"satMin":15,"satMax":19,"satVariance":1,"bright":74,"brightMin":72,"brightMax":76,"brightVariance":1,"alpha":100},{"name":"sMidBlue","hue":210,"hueMin":208,"hueMax":212,"hueVariance":1,"sat":50,"satMin":48,"satMax":52,"satVariance":1,"bright":55,"brightMin":53,"brightMax":57,"brightVariance":1,"alpha":100},{"name":"sMidBlue2","hue":210,"hueMin":208,"hueMax":212,"hueVariance":1,"sat":50,"satMin":48,"satMax":52,"satVariance":1,"bright":50,"brightMin":48,"brightMax":52,"brightVariance":1,"alpha":100},{"name":"sMidYellowGreen","hue":70,"hueMin":68,"hueMax":72,"hueVariance":1,"sat":20,"satMin":18,"satMax":22,"satVariance":1,"bright":55,"brightMin":53,"brightMax":57,"brightVariance":1,"alpha":100},{"name":"sSand","hue":40,"hueMin":38,"hueMax":42,"hueVariance":0.2,"sat":14,"satMin":12,"satMax":16,"satVariance":0.2,"bright":85,"brightMin":83,"brightMax":87,"brightVariance":0.2,"alpha":100},{"name":"sSlate","hue":55,"hueMin":53,"hueMax":57,"hueVariance":1,"sat":12,"satMin":10,"satMax":14,"satVariance":1,"bright":70,"brightMin":68,"brightMax":72,"brightVariance":1,"alpha":100},{"name":"sYellow","hue":41,"hueMin":39,"hueMax":43,"hueVariance":1,"sat":82,"satMin":80,"satMax":84,"satVariance":1,"bright":95,"brightMin":93,"brightMax":97,"brightVariance":1,"alpha":100},{"name":"sYellow2","hue":41,"hueMin":39,"hueMax":43,"hueVariance":1,"sat":82,"satMin":80,"satMax":84,"satVariance":1,"bright":90,"brightMin":88,"brightMax":92,"brightVariance":1,"alpha":100},{"name":"slDarkBlue","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":75,"satMin":73,"satMax":77,"satVariance":1,"bright":25,"brightMin":23,"brightMax":27,"brightVariance":1,"alpha":100},{"name":"slDarkBlue2","hue":220,"hueMin":218,"hueMax":222,"hueVariance":1,"sat":40,"satMin":38,"satMax":42,"satVariance":1,"bright":20,"brightMin":18,"brightMax":22,"brightVariance":1,"alpha":100},{"name":"slDarkBlue3","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":65,"satMin":63,"satMax":67,"satVariance":1,"bright":35,"brightMin":33,"brightMax":37,"brightVariance":1,"alpha":100},{"name":"slDarkGray","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":32,"satMin":30,"satMax":34,"satVariance":1,"bright":30,"brightMin":28,"brightMax":32,"brightVariance":1,"alpha":100},{"name":"slDarkGray2","hue":220,"hueMin":218,"hueMax":222,"hueVariance":1,"sat":38,"satMin":36,"satMax":40,"satVariance":1,"bright":30,"brightMin":28,"brightMax":32,"brightVariance":1,"alpha":100},{"name":"slDarkGray3","hue":220,"hueMin":218,"hueMax":222,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":30,"brightMin":28,"brightMax":32,"brightVariance":1,"alpha":100},{"name":"slDarkGrayPurple","hue":240,"hueMin":238,"hueMax":242,"hueVariance":1,"sat":25,"satMin":23,"satMax":27,"satVariance":1,"bright":30,"brightMin":28,"brightMax":32,"brightVariance":1,"alpha":100},{"name":"slLightBlue","hue":210,"hueMin":208,"hueMax":212,"hueVariance":1,"sat":23,"satMin":21,"satMax":25,"satVariance":1,"bright":65,"brightMin":63,"brightMax":67,"brightVariance":1,"alpha":100},{"name":"slLightPink","hue":352,"hueMin":350,"hueMax":354,"hueVariance":1,"sat":27,"satMin":25,"satMax":29,"satVariance":1,"bright":80,"brightMin":78,"brightMax":82,"brightVariance":1,"alpha":100},{"name":"slLightPink2","hue":335,"hueMin":333,"hueMax":337,"hueVariance":1,"sat":27,"satMin":25,"satMax":29,"satVariance":1,"bright":80,"brightMin":78,"brightMax":82,"brightVariance":1,"alpha":100},{"name":"slLightPurple","hue":320,"hueMin":318,"hueMax":322,"hueVariance":1,"sat":18,"satMin":16,"satMax":20,"satVariance":1,"bright":58,"brightMin":56,"brightMax":60,"brightVariance":1,"alpha":100},{"name":"slLightPurple2","hue":290,"hueMin":288,"hueMax":292,"hueVariance":1,"sat":24,"satMin":22,"satMax":26,"satVariance":1,"bright":58,"brightMin":56,"brightMax":60,"brightVariance":1,"alpha":100},{"name":"slMidBlue","hue":215,"hueMin":213,"hueMax":217,"hueVariance":1,"sat":45,"satMin":43,"satMax":47,"satVariance":1,"bright":58,"brightMin":56,"brightMax":60,"brightVariance":1,"alpha":100},{"name":"slMidPink","hue":352,"hueMin":350,"hueMax":354,"hueVariance":1,"sat":43,"satMin":41,"satMax":45,"satVariance":1,"bright":83,"brightMin":81,"brightMax":85,"brightVariance":1,"alpha":100},{"name":"slMidPink2","hue":340,"hueMin":338,"hueMax":342,"hueVariance":1,"sat":32,"satMin":30,"satMax":34,"satVariance":1,"bright":80,"brightMin":78,"brightMax":82,"brightVariance":1,"alpha":100},{"name":"slOffWhite","hue":40,"hueMin":38,"hueMax":42,"hueVariance":1,"sat":1,"satMin":0,"satMax":3,"satVariance":0.2,"bright":85,"brightMin":83,"brightMax":87,"brightVariance":0.2,"alpha":100},{"name":"slWarmWhite","hue":35,"hueMin":33,"hueMax":37,"hueVariance":1,"sat":10,"satMin":8,"satMax":12,"satVariance":0.2,"bright":78,"brightMin":76,"brightMax":80,"brightVariance":0.2,"alpha":100}],"palettes":[{"swatches":["aMidBlue","aDarkBlue","aIndigo","aPink","aAquamarine","aLightGreen","aYellow","aOrange","aDarkBrown","aPeach"],"colorSeq":["aPeach","aMidBlue","aDarkBlue","aIndigo","aPink","aAquamarine","aLightGreen","aYellow","aOrange","aDarkBrown"],"backgroundColors":[["Austin White",1,{}],["Austin Green",1,{"aMidBlue":null,"aAquamarine":null,"aYellow":"aYellow3","aOrange":"aOrange4","aDarkBrown":"aDarkBrown3","aIndigo":"aIndigo2","aPeach":null}],["Austin Yellow",1,{"aYellow":null,"aPeach":null,"aPink":null,"aMidBlue":null}],["Austin Blue",1,{"aMidBlue":null,"aAquamarine":"aAquamarine3","aYellow":"aYellow3","aOrange":"aOrange3","aDarkBrown":"aDarkBrown3","aIndigo":"aIndigo2","aPeach":null}]],"splatterColors":[["aPeach",1],["aPink",1],["aOrange",1],["aAquamarine",1],["aLightGreen",1],["aYellow",1]],"name":"austin"},{"swatches":["bGrayBlue","bIntenseBlue","bDimOrange","bRed","bDarkRed","bIntenseBlue2","bIndigo","bCoolDarkGreen","bGrayPurple"],"colorSeq":["bGrayBlue","bIntenseBlue","bRed","bDarkRed","bGrayBlue","bIntenseBlue","bIntenseBlue2","bIndigo","bDimOrange","bCoolDarkGreen","bGrayPurple","bIntenseBlue","bRed","bIntenseBlue","bIndigo","bDimOrange","bCoolDarkGreen"],"backgroundColors":[["Berlin Dark Green",1,{"bIndigo":"bIndigo2","bGrayPurple":"bGrayPurple2","bGrayBlue":"bGrayBlue2","bIntenseBlue":"bIntenseBlue3","bCoolDarkGreen":"bCoolDarkGreen2"}],["Berlin Gray Blue",1,{"bGrayPurple":"bGrayPurple3","bIndigo":null}],["Berlin Cool Purple",1,{"bGrayBlue":"bGrayBlue2","bIntenseBlue":"bIntenseBlue3","bGrayPurple":"bGrayPurple2"}],["Berlin Warm Purple",1,{"bGrayBlue":"bGrayBlue3","bGrayPurple":"bGrayPurple4","bIntenseBlue":"bIntenseBlue4","bIndigo":"bIndigo2","bCoolDarkGreen":"bCoolDarkGreen2"}],["Berlin Intense Blue",1,{"bGrayBlue":null,"bIntenseBlue":null,"bIndigo":"bIndigo2","bGrayPurple":"bGrayPurple4","bCoolDarkGreen":"bCoolDarkGreen2"}]],"splatterColors":[["bRed",0.1],["bGrayBlue",0.2],["bGrayPurple",0.1],["bDimOrange",0.1],["bCoolDarkGreen",0.1],["bCoolMidGreen",0.1]],"name":"berlin"},{"swatches":["eCream","ePink","eRed","eGrayBlue","eBrown","eRedOrange","eOrange","eYellow","ePaleYellow","eCoolDarkGreen","eMidGreen"],"colorSeq":["ePink","eRed","eCream","eCream","eGrayBlue","eGrayBlue","eBrown","eRedOrange","eOrange","eYellow","ePaleYellow","eCoolDarkGreen","eMidGreen","eGrayBlue","eCream"],"backgroundColors":[["Edinburgh Tan",3,{"ePaleYellow":null}],["Edinburgh Blue",2,{"eBrown":"eBrown2","eCream":"eCream2"}],["Edinburgh Green",2,{"eGrayBlue":"eGrayBlue2","eBrown":"eBrown3","eCream":"eCream2"}],["Edinburgh Dark Brown",2,{"eCoolDarkGreen":null,"eCream":"eCream2","eBrown":"eBrown4"}],["Edinburgh Mid Brown",1,{"eBrown":"eBrown2","eCoolDarkGreen":"eCoolDarkGreen2","eGrayBlue":"eGrayBlue3","ePaleYellow":"ePaleYellow2","ePink":"ePink2","eRed":"eRed2"}]],"splatterColors":[["ePink",0.1],["eRed",0.1],["eCream",0.1],["eYellow",0.1],["ePaleYellow",0.1],["eGrayBlue",0.1],["eBrown",0.1],["eOrange",0.1],["eCoolDarkGreen",0.1],["eMidGreen",0.1]],"name":"edinburgh"},{"swatches":["fDarkRed","fRed","fOrange","fPaleYellow","fYellow","fPink","fNewsprint","fPaleGreen","fGreen","fExtraDarkBlue","fDarkBlue","fBlue","fPaleBlue","fBrown","fDarkBrown","fExtraDarkBrown"],"colorSeq":["fDarkRed","fRed","fNewsprint","fOrange","fPaleYellow","fYellow","fPink","fGreen","fPaleGreen","fExtraDarkBlue","fDarkBlue","fBlue","fPaleBlue","fBrown","fDarkBrown","fExtraDarkBrown"],"backgroundColors":[["Fidenza Newsprint",6,{}],["Fidenza Dark Blue",3,{}],["Fidenza Light Green",2,{}],["Fidenza Green",2,{}],["Fidenza Brown",2,{}],["Fidenza Pink",1,{}]],"splatterColors":[["fRed",1],["fYellow",1],["fGreen",1],["fBlue",1]],"name":"fidenza"},{"swatches":["mWhite","mPink","mBlue","mLightBlue","mOrange","mYellow","mGreen","mPurple"],"colorSeq":["mPink","mWhite","mBlue","mLightBlue","mOrange","mYellow","mGreen","mPurple","mPink","mWhite","mBlue","mLightBlue","mYellow","mGreen","mPink","mWhite","mBlue","mLightBlue","mYellow","mGreen"],"backgroundColors":[["Miami White",6,{"mWhite":null}],["Miami Rich Blue",3,{"mGreen":null,"mBlue":"mBlue2","mLightBlue":"mLightBlue2","mPink":"mPink2","mYellow":"mYellow2","mPurple":"mPurple2"}],["Miami Dark Purple",1,{}]],"splatterColors":[["mPink",1],["mWhite",1],["mYellow",1],["mTeal",1]],"name":"miami"},{"swatches":["sLightBlue","sMidBlue","sLightYellow","sYellow","sLightYellowGreen","sMidYellowGreen","sDarkGreen","sDarkMidGreen","sDarkGrayBrown","sDarkBlue","sSlate","sSand"],"colorSeq":["sLightBlue","sMidBlue","sLightYellow","sYellow","sLightYellowGreen","sMidYellowGreen","sDarkGreen","sDarkMidGreen","sDarkGrayBrown","sDarkBlue","sSlate","sLightBlue","sMidBlue","sLightYellow","sLightYellowGreen","sMidYellowGreen","sSand","sDarkGreen","sDarkMidGreen","sDarkGrayBrown","sDarkBlue","sSlate"],"backgroundColors":[["Seattle Warm Gray",1,{"sLightYellow":null,"sYellow":"sYellow2","sLightYellowGreen":null,"sDarkBlue":"sDarkBlue2"}],["Seattle Mid Blue",1,{"sMidYellowGreen":null,"sMidBlue":"sMidBlue2","sLightBlue":"sLightBlue2"}],["Seattle Dark Gray",1.5,{"sDarkGrayBrown":"sDarkGrayBrown2"}]],"splatterColors":[["sYellow",1],["sLightBlue",1],["sMidBlue",1],["sLightYellowGreen",1]],"name":"seattle"},{"swatches":["slLightBlue","slMidBlue","slDarkGray","slMidPink","slLightPink","slWarmWhite","slLightPurple","slDarkGrayPurple","slDarkBlue"],"colorSeq":["slLightBlue","slMidBlue","slDarkBlue","slMidPink","slLightPink","slDarkGrayPurple","slLightPurple","slDarkGray","slWarmWhite"],"backgroundColors":[["Seoul Light Pink",3,{"slLightPink":null,"slWarmWhite":"slOffWhite"}],["Seoul Mid Blue",3,{"slDarkBlue":"slDarkBlue3","slDarkGray":"slDarkGray3","slLightPurple":"slLightPurple2"}],["Seoul Dark Blue",2,{}],["Seoul Light Blue",3,{"slMidPink":"slMidPink2","slLightPink":"slLightPink2","slLightBlue":null,"slLightPurple":"slLightPurple2"}],["Seoul Light Gray",4,{"slWarmWhite":"slOffWhite"}],["Seoul Dark Gray",4,{"slDarkGray":"slDarkGray2","slDarkBlue":"slDarkBlue2"}],["Seoul Mid Pink",1,{"slMidPink":null,"slLightBlue":null,"slLightPurple":"slLightPurple2"}]],"splatterColors":[["slMidBlue",1],["slMidPink",1],["slLightPink",1],["slWarmWhite",1]],"name":"seoul"}]} 2 | --------------------------------------------------------------------------------