├── .envrc ├── .gitignore ├── .rustfmt.toml ├── .travis.yml ├── @types ├── p5.d.ts └── p5_module.d.ts ├── Cargo.lock ├── Cargo.toml ├── README.md ├── dist ├── 404.md ├── bundle.js ├── bundle.js.map └── index.html ├── geo ├── Cargo.toml └── src │ ├── angle.rs │ ├── bbox.rs │ ├── convex_hull.rs │ ├── delaunay.rs │ ├── kdtree.rs │ ├── kmeans.rs │ ├── lib.rs │ ├── line.rs │ ├── point.rs │ ├── polygon.rs │ ├── triangle.rs │ └── utils │ ├── ksmallest.rs │ ├── mod.rs │ └── ordwrapper.rs ├── images ├── baboon-quantized.jpeg ├── baboon.jpeg ├── delaunay.png ├── desert-quantized.jpeg ├── desert.jpeg ├── fractree.png ├── mandelbrot.png ├── mondrian1.png ├── mondrian2.png ├── patchwork-filled1.png ├── patchwork-filled2.png ├── patchwork.png ├── rb-primitized.png ├── rb.png ├── red-horns.png ├── redblue-dragon.png ├── runes.png ├── sierpinski.png ├── stippling-gradient.png ├── stippling-rects.png ├── tangled-web.png ├── tiffanys-primitized.png ├── tiffanys.jpg ├── voronoi-gradient.png └── voronoi.png ├── index.html ├── index.scss ├── index.tsx ├── mattopy ├── Pipfile ├── Pipfile.lock ├── buildings.py ├── fractures.py ├── mountains.py ├── pylintrc └── quads.py ├── mattors ├── art │ ├── delaunay.rs │ ├── dithering.rs │ ├── dragon.rs │ ├── fractree.rs │ ├── julia.rs │ ├── mod.rs │ ├── mondrian.rs │ ├── patchwork.rs │ ├── primi.rs │ ├── quantize.rs │ ├── runes.rs │ ├── sierpinski.rs │ ├── stippling.rs │ ├── tangled_web.rs │ └── voronoi.rs ├── color │ └── mod.rs ├── drawing │ ├── line.rs │ ├── mod.rs │ └── triangle.rs ├── lib.rs └── main.rs ├── package.json ├── postcss.config.js ├── scripts ├── cargo-debug-macro └── deploy ├── sketches ├── annulus.ts ├── astroid.ts ├── blankets.ts ├── bloody-spider-web.ts ├── bw-rain.ts ├── cairo-tiling.ts ├── chaikin.ts ├── circular-maze.ts ├── clifford-attractors.ts ├── cubic-disarray.ts ├── cuts.ts ├── dla.ts ├── eyes.ts ├── focus-eye.ts ├── galaxy-map.ts ├── isolines.ts ├── light-in-a-cave.ts ├── neon-lines.ts ├── noise-quads.ts ├── nucleus.ts ├── parallel-bands.ts ├── penrose-tiling.ts ├── print10.ts ├── roses.ts ├── rots.ts ├── rough-balls.ts ├── scribbles.ts ├── sketch.ts ├── sorting.ts ├── space-filling-curves.ts ├── spiral-christmas-tree.ts ├── spiral-noise.ts ├── super-permutations.ts ├── symmetry.ts ├── triangular-maze.ts ├── truchet-tiles.ts ├── utils.ts ├── vornoi.ts └── walls.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.envrc: -------------------------------------------------------------------------------- 1 | PATH_add `expand_path scripts` 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rust 2 | target 3 | *.rs.bk 4 | 5 | # wasm-pack 6 | pkg 7 | 8 | # node 9 | node_modules 10 | .cache 11 | 12 | # images 13 | *.png 14 | *.svg 15 | !images/*.png 16 | !images/*.svg 17 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | 4 | rust: 5 | - stable 6 | - beta 7 | - nightly 8 | 9 | matrix: 10 | allow_failures: 11 | - rust: nightly 12 | fast_finish: true 13 | -------------------------------------------------------------------------------- /@types/p5_module.d.ts: -------------------------------------------------------------------------------- 1 | // p5 typings don't ship this... 2 | declare module "p5"; 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "matto" 3 | version = "0.1.0" 4 | authors = ["daniele "] 5 | edition = "2018" 6 | 7 | [workspace] 8 | members = [ 9 | "geo", 10 | ] 11 | 12 | [lib] 13 | path = "mattors/lib.rs" 14 | 15 | [[bin]] 16 | name = "matto" 17 | path = "mattors/main.rs" 18 | 19 | [dependencies] 20 | geo = { path = "./geo" } 21 | 22 | image = "0.22" 23 | num = "0.2" 24 | rand = "0.7" 25 | structopt = "0.3" 26 | 27 | [dev-dependencies] 28 | maplit = "1.0" 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matto [![Build Status](https://travis-ci.org/danieledapo/mattors.svg?branch=master)](https://travis-ci.org/danieledapo/mattors) 2 | 3 | Have some fun visualizing math. 4 | 5 | ## Fractals 6 | 7 | ![redblue dragon](images/redblue-dragon.png) 8 | ![mandelbrot](images/mandelbrot.png) 9 | ![horns](images/red-horns.png) 10 | ![fractal tree](images/fractree.png) 11 | ![sierpinski](images/sierpinski.png) 12 | 13 | ## Quantization 14 | 15 | ![desert-quantized](images/desert-quantized.jpeg) 16 | ![baboon-quantized](images/baboon-quantized.jpeg) 17 | 18 | ## Primirs 19 | 20 | inspired by [primitive](https://github.com/fogleman/primitive). 21 | 22 | ![rb-primitized](images/rb-primitized.png) 23 | ![tiffanys-primitized](images/tiffanys-primitized.png) 24 | 25 | ## Voronoi 26 | 27 | ![voronoi](images/voronoi.png) 28 | ![voronoi-gradient](images/voronoi-gradient.png) 29 | 30 | ## Delaunay 31 | 32 | ![delaunay](images/delaunay.png) 33 | 34 | ## Patchwork 35 | 36 | inspired by [this article](https://mattdesl.svbtle.com/pen-plotter-2). 37 | 38 | ![patchwork](images/patchwork.png) 39 | ![patchwork-filled1](images/patchwork-filled1.png) 40 | ![patchwork-filled2](images/patchwork-filled2.png) 41 | 42 | ## Stippling 43 | 44 | ![stippling-gradient](images/stippling-gradient.png) 45 | ![stippling-rects](images/stippling-rects.png) 46 | 47 | ## Mondrian 48 | 49 | inspired by `Composition in Red, Blue and Yellow` by Mondrian. 50 | 51 | ![mondrian1](images/mondrian1.png) 52 | ![mondrian2](images/mondrian2.png) 53 | 54 | ## Tangled webs 55 | 56 | inspired by https://www.inconvergent.net/2019/a-tangle-of-webs/. 57 | 58 | ![tangled-webs](images/tangled-web.png) 59 | 60 | # Examples 61 | 62 | ``` 63 | # fractals 64 | cargo run -- dragons 65 | cargo run -- horns 66 | cargo run -- julia 67 | cargo run -- julia --iterations 16 mandelbrot 68 | cargo run -- julia --iterations 128 custom -c ' -0.4+0.6i' --start " -3.0,-1.2" --end "2.0,1.2" 69 | cargo run -- sierpinski --fancy 70 | cargo run -- fractal-tree 71 | 72 | # quantize 73 | cargo run -- quantize images/desert.jpeg -o images/desert-quantized.jpeg 74 | cargo run -- quantize -d 1 images/baboon.jpeg -o images/baboon-quantized.jpeg 75 | 76 | # primirs 77 | cargo run --release -- primirs --shapes 200 --mutations 150 -o images/rb-primitized.png --dx 8 --dy 8 images/rb.png 78 | cargo run --release -- primirs --shapes 200 --mutations 100 --scale-down 2 --dx 16 --dy 16 -o primitized.png images/tiffanys.jpg 79 | 80 | # voronoi 81 | cargo run -- voronoi --points 150 -o images/voronoi.png 82 | cargo run -- voronoi --gradient-background --points 150 -o images/voronoi-gradient.png 83 | 84 | # delaunay 85 | cargo run -- delaunay --grid-size 50 -o images/delaunay.png 86 | 87 | # patchwork 88 | cargo run -- patchwork 89 | cargo run --release -- patchwork -f --points 4000 --width 600 --height 600 --clusters 10 90 | 91 | # stippling 92 | cargo run -- stippling gradient -p 1000 -k 5 93 | cargo run -- stippling rects --iterations 1500 94 | 95 | # Mondrian 96 | cargo run -- mondrian -w 800 -h 800 97 | 98 | # tangled webs 99 | cargo run -- tangled-web 100 | 101 | # misc 102 | cargo run -- runes -p 3 -c 26 103 | cargo run -- dither -c 2 images/desert.jpeg 104 | cargo run -- dither -c 5 --rgb images/desert.jpeg 105 | ``` 106 | -------------------------------------------------------------------------------- /dist/404.md: -------------------------------------------------------------------------------- 1 | These are not the droids you're looking for. 2 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Matto 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /geo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "geo" 3 | version = "0.1.0" 4 | authors = ["Daniele D'Orazio "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | num = "0.2" 9 | 10 | [dev-dependencies] 11 | proptest = "0.9" 12 | -------------------------------------------------------------------------------- /geo/src/angle.rs: -------------------------------------------------------------------------------- 1 | //! Module that contains simple utilities to work with angles. 2 | 3 | use std::cmp::Ordering; 4 | 5 | use crate::point::Point; 6 | use crate::utils::cmp_floats; 7 | 8 | /// The orientation of an angle, for example between three points. 9 | #[derive(Debug, Clone, Copy, PartialEq)] 10 | pub enum AngleOrientation { 11 | /// Counter-clockwise direction 12 | CounterClockwise, 13 | 14 | /// Clockwsise direction 15 | Clockwise, 16 | 17 | /// Colinear direction 18 | Colinear, 19 | } 20 | 21 | /// Calculate the polar angle between the two points. 22 | pub fn polar_angle(p1: &Point, p2: &Point) -> f64 { 23 | f64::atan2(p2.y - p1.y, p2.x - p1.x) 24 | } 25 | 26 | /// Calculate the angle orientation between three points where p2 is the center 27 | /// point. 28 | pub fn angle_orientation(p1: &Point, p2: &Point, p3: &Point) -> AngleOrientation { 29 | let area = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x); 30 | 31 | match cmp_floats(area, 0.0) { 32 | Ordering::Equal => AngleOrientation::Colinear, 33 | Ordering::Less => AngleOrientation::Clockwise, 34 | Ordering::Greater => AngleOrientation::CounterClockwise, 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::{angle_orientation, AngleOrientation}; 41 | 42 | use geo::PointF64; 43 | 44 | #[test] 45 | fn test_angle_orientation() { 46 | assert_eq!( 47 | angle_orientation( 48 | &PointF64::new(0.0, 0.0), 49 | &PointF64::new(2.0, 2.0), 50 | &PointF64::new(0.0, 0.0) 51 | ), 52 | AngleOrientation::Colinear 53 | ); 54 | 55 | assert_eq!( 56 | angle_orientation( 57 | &PointF64::new(0.0, 0.0), 58 | &PointF64::new(2.0, 2.0), 59 | &PointF64::new(4.0, 0.0), 60 | ), 61 | AngleOrientation::Clockwise 62 | ); 63 | 64 | assert_eq!( 65 | angle_orientation( 66 | &PointF64::new(0.0, 0.0), 67 | &PointF64::new(4.0, 0.0), 68 | &PointF64::new(2.0, 2.0), 69 | ), 70 | AngleOrientation::CounterClockwise 71 | ); 72 | 73 | assert_eq!( 74 | angle_orientation( 75 | &PointF64::new(4.0, 0.0), 76 | &PointF64::new(4.0, 0.0), 77 | &PointF64::new(2.0, 2.0), 78 | ), 79 | AngleOrientation::Colinear 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /geo/src/convex_hull.rs: -------------------------------------------------------------------------------- 1 | //! Module that allows to compute the [Convex 2 | //! Hull](https://en.wikipedia.org/wiki/Convex_hull) of a set of points. 3 | 4 | use std::cmp::Ordering; 5 | 6 | use crate::angle::{angle_orientation, polar_angle, AngleOrientation}; 7 | use crate::point::Point; 8 | use crate::utils::cmp_floats; 9 | 10 | /// Calculate the convex hull of a set of points and return the points that 11 | /// compose the convex hull. 12 | pub fn convex_hull(points: I) -> Vec> 13 | where 14 | I: IntoIterator>, 15 | { 16 | let mut points = points.into_iter().collect::>(); 17 | 18 | if points.len() < 2 { 19 | return points; 20 | } 21 | 22 | let lowest_point = *points 23 | .iter() 24 | .min_by(|p1, p2| { 25 | let ycmp = cmp_floats(p1.y, p2.y); 26 | 27 | if let Ordering::Equal = ycmp { 28 | cmp_floats(p1.x, p2.x) 29 | } else { 30 | ycmp 31 | } 32 | }) 33 | .unwrap(); 34 | 35 | // sort in descending order so that we remove points from the back which is 36 | // amortized O(1). 37 | points.sort_unstable_by(|p1, p2| { 38 | let a1 = polar_angle(&lowest_point, p1); 39 | let a2 = polar_angle(&lowest_point, p2); 40 | 41 | let angle_cmp = cmp_floats(a2, a1); 42 | 43 | if let Ordering::Equal = angle_cmp { 44 | let ycmp = cmp_floats(p2.y, p1.y); 45 | 46 | if let Ordering::Equal = ycmp { 47 | cmp_floats(p2.x, p1.x) 48 | } else { 49 | ycmp 50 | } 51 | } else { 52 | angle_cmp 53 | } 54 | }); 55 | 56 | let mut hull = vec![]; 57 | hull.push(points.pop().unwrap()); 58 | hull.push(points.pop().unwrap()); 59 | 60 | for point in points.into_iter().rev() { 61 | while hull.len() >= 2 { 62 | let orientation = 63 | angle_orientation(&hull[hull.len() - 2], hull.last().unwrap(), &point); 64 | 65 | match orientation { 66 | AngleOrientation::Clockwise | AngleOrientation::Colinear => hull.pop(), 67 | AngleOrientation::CounterClockwise => break, 68 | }; 69 | } 70 | 71 | hull.push(point); 72 | } 73 | 74 | hull 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::convex_hull; 80 | 81 | use proptest::prelude::*; 82 | 83 | use geo::{Point, Polygon}; 84 | 85 | #[test] 86 | fn test_convex_hull() { 87 | let points = vec![ 88 | Point::new(392.0, 23.0), 89 | Point::new(134.0, 59.0), 90 | Point::new(251.0, 127.0), 91 | Point::new(266.0, 143.0), 92 | Point::new(380.0, 183.0), 93 | Point::new(337.0, 44.0), 94 | Point::new(229.0, 20.0), 95 | Point::new(378.0, 496.0), 96 | Point::new(392.0, 23.0), 97 | ]; 98 | 99 | let hull = convex_hull(points); 100 | 101 | assert_eq!( 102 | hull, 103 | vec![ 104 | Point::new(229.0, 20.0), 105 | Point::new(392.0, 23.0), 106 | Point::new(378.0, 496.0), 107 | Point::new(134.0, 59.0), 108 | ] 109 | ); 110 | } 111 | 112 | #[test] 113 | fn test_convex_hull_multiple_points_on_same_y() { 114 | let points = vec![ 115 | Point::new(4.0, 40.0), 116 | Point::new(21.0, 21.0), 117 | Point::new(37.0, 32.0), 118 | Point::new(40.0, 21.0), 119 | ]; 120 | 121 | let hull = convex_hull(points); 122 | 123 | assert_eq!( 124 | hull, 125 | vec![ 126 | Point::new(21.0, 21.0), 127 | Point::new(40.0, 21.0), 128 | Point::new(37.0, 32.0), 129 | Point::new(4.0, 40.0), 130 | ] 131 | ); 132 | } 133 | 134 | #[test] 135 | fn test_convex_hull_colinear() { 136 | let points = vec![ 137 | Point::new(12.0, 41.0), 138 | Point::new(17.0, 36.0), 139 | Point::new(42.0, 11.0), 140 | Point::new(0.0, 12.0), 141 | ]; 142 | 143 | let hull = convex_hull(points); 144 | 145 | assert_eq!( 146 | hull, 147 | vec![ 148 | Point::new(42.0, 11.0), 149 | Point::new(12.0, 41.0), 150 | Point::new(0.0, 12.0), 151 | ] 152 | ); 153 | } 154 | 155 | proptest! { 156 | #![proptest_config(proptest::test_runner::Config::with_cases(500))] 157 | #[test] 158 | fn prop_convex_contains_all_the_points( 159 | points in prop::collection::hash_set((0_u8..255, 0_u8..255), 3..100) 160 | ) { 161 | let points = points 162 | .into_iter() 163 | .map(|(x, y)| Point::new(f64::from(x), f64::from(y))) 164 | .collect::>(); 165 | 166 | let hull = convex_hull(points.clone()); 167 | prop_assume!(hull.len() > 2); 168 | 169 | let hull = Polygon::new(hull).unwrap(); 170 | 171 | for pt in &points { 172 | assert!( 173 | hull.contains(pt), 174 | "points {:?} hull {:?} point {:?}", 175 | points, 176 | hull, 177 | pt 178 | ); 179 | } 180 | } 181 | } 182 | 183 | proptest! { 184 | #![proptest_config(proptest::test_runner::Config::with_cases(100))] 185 | #[test] 186 | fn prop_convex_hull_lies_on_boundary( 187 | points in prop::collection::vec((0_u8..255, 0_u8..255), 1..100) 188 | ) { 189 | _prop_convex_hull_lies_on_boundary(points) 190 | } 191 | } 192 | 193 | fn _prop_convex_hull_lies_on_boundary(points: Vec<(u8, u8)>) { 194 | let points = points 195 | .into_iter() 196 | .map(|(x, y)| Point::new(x, y)) 197 | .collect::>(); 198 | 199 | let hull = convex_hull(points.iter().map(|p| p.cast::())); 200 | 201 | for pt in hull { 202 | let pt = Point::new(pt.x as u8, pt.y as u8); 203 | 204 | let on_boundary = points.iter().find(|h| **h == pt).is_some(); 205 | 206 | assert!(on_boundary); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /geo/src/delaunay.rs: -------------------------------------------------------------------------------- 1 | //! Simple module that implements [Delaunay 2 | //! triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation) 3 | 4 | use crate::bbox::BoundingBox; 5 | use crate::point::{Point, PointF64}; 6 | use crate::triangle::Triangle; 7 | 8 | /// Triangulate the given set of points. This blows up if degenerate triangles 9 | /// are formed(e.g. completely flat triangles). 10 | pub fn triangulate(bounding_box: &BoundingBox, points: Vec) -> Vec> { 11 | if points.len() < 3 { 12 | return vec![]; 13 | } 14 | 15 | let mut points = points.into_iter(); 16 | let super_triangles = super_triangles(bounding_box, &points.next().unwrap()); 17 | 18 | // theoretically we should remove the triangles that share vertices with the 19 | // initial point, but this thing is not for real use. 20 | 21 | points.fold(super_triangles, |triangles, point| { 22 | add_point(triangles, &point) 23 | }) 24 | } 25 | 26 | // the original algorithm works by finding a super triangle that encloses 27 | // all the points, but since we live in a finite space just pickup a random 28 | // point and divide the bounding box in 4 triangles that always cover the 29 | // entire space. It's not acceptable for real triangulation but we're having 30 | // fun here :). 31 | fn super_triangles(bounding_box: &BoundingBox, first_point: &PointF64) -> Vec> { 32 | let bounds = bounding_box.points(); 33 | 34 | (0..bounds.len()) 35 | .map(|i| Triangle::new(bounds[i], bounds[(i + 1) % bounds.len()], *first_point)) 36 | .collect() 37 | } 38 | 39 | fn add_point(triangles: Vec>, point: &Point) -> Vec> { 40 | let mut edges = vec![]; 41 | let mut new_triangles = Vec::with_capacity(triangles.len()); 42 | 43 | for triangle in triangles { 44 | let (circumcenter, radius) = triangle.squared_circumcircle().unwrap(); 45 | 46 | if circumcenter.squared_dist::(point) <= radius { 47 | edges.push((triangle.points[0], triangle.points[1])); 48 | edges.push((triangle.points[1], triangle.points[2])); 49 | edges.push((triangle.points[2], triangle.points[0])); 50 | } else { 51 | new_triangles.push(triangle); 52 | } 53 | } 54 | 55 | edges = dedup_edges(&edges); 56 | 57 | new_triangles.extend( 58 | edges 59 | .into_iter() 60 | .map(|(pt0, pt1)| Triangle::new(pt0, pt1, *point)), 61 | ); 62 | 63 | new_triangles 64 | } 65 | 66 | fn dedup_edges(edges: &[(Point, Point)]) -> Vec<(Point, Point)> { 67 | // super ugly and super inefficient, but we cannot use hashmaps with f64... 68 | 69 | let mut out = vec![]; 70 | 71 | for i in 0..edges.len() { 72 | let mut count = 0; 73 | 74 | for j in 0..edges.len() { 75 | let (start, end) = &edges[j]; 76 | if edges[i] == (*start, *end) || edges[i] == (*end, *start) { 77 | count += 1; 78 | } 79 | } 80 | 81 | if count == 1 { 82 | out.push(edges[i]); 83 | } 84 | } 85 | 86 | out 87 | } 88 | 89 | #[cfg(test)] 90 | mod test { 91 | use super::dedup_edges; 92 | 93 | use geo::Point; 94 | 95 | #[test] 96 | fn test_dedup_edges() { 97 | let edge1 = (Point::new(42.0, 12.0), Point::new(7.0, 12.0)); 98 | let redge1 = (edge1.1, edge1.0); 99 | 100 | let edge2 = (Point::new(42.0, 73.0), Point::new(84.0, 146.0)); 101 | let redge2 = (edge2.1, edge2.0); 102 | 103 | let edge3 = (Point::new(23.0, 32.0), Point::new(32.0, 23.0)); 104 | 105 | let edges = vec![edge1, edge2, edge1, redge2, edge3, redge1, edge1, redge1]; 106 | 107 | assert_eq!(dedup_edges(&edges), vec![edge3]); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /geo/src/kmeans.rs: -------------------------------------------------------------------------------- 1 | //! A simple [K-Means](https://en.wikipedia.org/wiki/K-means_clustering) 2 | //! implementation. 3 | 4 | use std::collections::{HashMap, HashSet}; 5 | use std::fmt::Debug; 6 | use std::hash::Hash; 7 | use std::iter; 8 | 9 | use crate::point::Point; 10 | 11 | /// Cluster the given set of points in at most k clusters. If k is greater or 12 | /// equal than the set of unique points then all the input points are returned. 13 | /// Note that K-Means doesn't return the optimal solution and in fact it's 14 | /// totally possible that the clusters contain less than k clusters. To avoid 15 | /// that try to increase the number of max_iterations and/or shuffle the points. 16 | pub fn kmeans(points: I, k: usize, max_iterations: usize) -> HashMap, Vec>> 17 | where 18 | T: num::Num + Ord + Copy + Hash + From + Debug, 19 | I: IntoIterator>, 20 | { 21 | if k == 0 { 22 | return HashMap::new(); 23 | } 24 | 25 | // first dedup points in an hashset and then store them in a vec. 26 | let points = points 27 | .into_iter() 28 | .collect::>() 29 | .into_iter() 30 | .collect::>(); 31 | 32 | if points.len() <= k { 33 | return points.into_iter().map(|p| (p, vec![p])).collect(); 34 | } 35 | 36 | let mut clusters = iter::repeat(vec![]).take(k).collect::>(); 37 | 38 | // don't want to pickup random values, the caller can always shuffle the 39 | // array to achieve the same effect. 40 | let mut pivots = (0..k) 41 | .map(|i| points[i * points.len() / k]) 42 | .collect::>(); 43 | 44 | for _ in 0..max_iterations { 45 | for cluster in &mut clusters { 46 | cluster.clear(); 47 | } 48 | 49 | for point in &points { 50 | let closest_i = pivots 51 | .iter() 52 | .enumerate() 53 | .min_by_key(|(i, p)| (p.squared_dist::(point), clusters[*i].len())) 54 | .unwrap() 55 | .0; 56 | 57 | clusters[closest_i].push(*point); 58 | } 59 | 60 | let pivot_changed = update_pivots(&mut pivots, &clusters, &points, k); 61 | if !pivot_changed { 62 | break; 63 | } 64 | } 65 | 66 | pivots 67 | .into_iter() 68 | .zip(clusters.into_iter()) 69 | .filter(|(_, c)| !c.is_empty()) 70 | .collect() 71 | } 72 | 73 | fn update_pivots( 74 | pivots: &mut [Point], 75 | clusters: &[Vec>], 76 | points: &[Point], 77 | k: usize, 78 | ) -> bool 79 | where 80 | T: num::Num + Copy + From + Debug, 81 | { 82 | let mut pivot_changed = false; 83 | 84 | for (i, pivot) in pivots.iter_mut().enumerate() { 85 | let new_pivot = if clusters[i].is_empty() { 86 | // if the cluster for this pivot is empty pickup a point that's 87 | // different from the current pivot and hope for the best. 88 | let new_pivot_ix = i * points.len() / k; 89 | let mut p = points[new_pivot_ix]; 90 | 91 | if p == *pivot { 92 | // since the points were deduped, if p is the pivot the next 93 | // point is definitely not. 94 | p = points[(new_pivot_ix + 1) % points.len()]; 95 | debug_assert_ne!(p, *pivot); 96 | } 97 | 98 | p 99 | } else { 100 | avg_point(&clusters[i]) 101 | }; 102 | 103 | if new_pivot != *pivot { 104 | pivot_changed = true; 105 | } 106 | 107 | *pivot = new_pivot; 108 | } 109 | 110 | pivot_changed 111 | } 112 | 113 | fn avg_point(cluster: &[Point]) -> Point 114 | where 115 | T: num::Num + Copy + From, 116 | { 117 | let (sum_x, sum_y, len) = cluster.iter().fold( 118 | (T::from(0_u8), T::from(0_u8), T::from(0_u8)), 119 | |(sum_x, sum_y, len), pt| (sum_x + pt.x, sum_y + pt.y, len + T::from(1)), 120 | ); 121 | 122 | Point::new(sum_x / len, sum_y / len) 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use super::kmeans; 128 | 129 | use proptest::prelude::*; 130 | 131 | use geo::Point; 132 | 133 | type PointK = Point; 134 | 135 | fn points_and_k() -> impl Strategy, usize)> { 136 | prop::collection::vec((-255_i32..255, -255_i32..255), 1..100).prop_flat_map(|points| { 137 | let points = points 138 | .into_iter() 139 | .map(|(x, y)| Point::new(x, y)) 140 | .collect::>(); 141 | let len = points.len(); 142 | 143 | (Just(points), 0..len) 144 | }) 145 | } 146 | 147 | proptest! { 148 | #![proptest_config(proptest::test_runner::Config::with_cases(500))] 149 | #[test] 150 | fn prop_kmeans_clusters_contains_closest_point( 151 | (points, k) in points_and_k() 152 | ) { 153 | _prop_kmeans_clusters_contains_closest_point(points, k) 154 | } 155 | } 156 | 157 | fn _prop_kmeans_clusters_contains_closest_point(points: Vec, k: usize) { 158 | let clusters = kmeans(points.clone(), k, usize::max_value()); 159 | assert!(clusters.len() <= k); 160 | 161 | for (pivot, cluster) in &clusters { 162 | assert!(!cluster.is_empty()); 163 | 164 | for point in cluster { 165 | let closest_pivot = clusters.keys().min_by_key(|p| p.squared_dist::(point)); 166 | assert!(closest_pivot.is_some()); 167 | 168 | let closest_pivot = closest_pivot.unwrap(); 169 | assert_eq!( 170 | point.squared_dist::(closest_pivot), 171 | point.squared_dist::(pivot) 172 | ); 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /geo/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Geometric functions, algorithms and data structures. 2 | 3 | pub mod angle; 4 | pub mod bbox; 5 | pub mod convex_hull; 6 | pub mod delaunay; 7 | pub mod kdtree; 8 | pub mod kmeans; 9 | pub mod line; 10 | pub mod point; 11 | pub mod polygon; 12 | pub mod triangle; 13 | pub mod utils; 14 | 15 | pub use self::angle::{angle_orientation, polar_angle, AngleOrientation}; 16 | pub use self::bbox::BoundingBox; 17 | pub use self::line::LineEquation; 18 | pub use self::point::{Point, PointF64, PointI32, PointU32}; 19 | pub use self::polygon::Polygon; 20 | pub use self::triangle::Triangle; 21 | -------------------------------------------------------------------------------- /geo/src/triangle.rs: -------------------------------------------------------------------------------- 1 | //! Module to work with triangles. 2 | 3 | use crate::line::LineEquation; 4 | use crate::point::Point; 5 | 6 | /// Simple Triangle shape. 7 | #[derive(Clone, Debug, PartialEq)] 8 | pub struct Triangle

{ 9 | /// The points of the triangle 10 | pub points: [Point

; 3], 11 | } 12 | 13 | impl

Triangle

14 | where 15 | P: num::Num + From + Copy, 16 | { 17 | /// Create a new `Triangle` from the given points. 18 | pub fn new(p1: Point

, p2: Point

, p3: Point

) -> Triangle

{ 19 | Triangle { 20 | points: [p1, p2, p3], 21 | } 22 | } 23 | 24 | /// Return the [centroid](https://en.wikipedia.org/wiki/Centroid) of the 25 | /// triangle. 26 | pub fn centroid(&self) -> Point

{ 27 | let (sum_x, sum_y) = self 28 | .points 29 | .iter() 30 | .fold((P::zero(), P::zero()), |(accx, accy), pt| { 31 | (accx + pt.x, accy + pt.y) 32 | }); 33 | 34 | let avg_x = sum_x / P::from(3); 35 | let avg_y = sum_y / P::from(3); 36 | 37 | Point::new(avg_x, avg_y) 38 | } 39 | } 40 | 41 | impl

Triangle

42 | where 43 | P: num::Num + num::Signed + From + Copy + PartialOrd, 44 | { 45 | /// Return the area for this triangle. 46 | pub fn area(&self) -> P { 47 | self.signed_area().abs() 48 | } 49 | 50 | /// Return the signed area for this triangle. The sign indicates the 51 | /// orientation of the points. If it's negative then the vertices are in 52 | /// clockwise order, counter clockwise otherwise. 53 | pub fn signed_area(&self) -> P { 54 | let parallelogram_area = (self.points[1].x - self.points[0].x) 55 | * (self.points[2].y - self.points[0].y) 56 | - (self.points[2].x - self.points[0].x) * (self.points[1].y - self.points[0].y); 57 | 58 | parallelogram_area / P::from(2) 59 | } 60 | 61 | /// Transform this triangle so that the vertices are always in counter 62 | /// clockwise order. 63 | pub fn counter_clockwise(self) -> Self { 64 | if self.area() < P::from(0) { 65 | self 66 | } else { 67 | Triangle::new(self.points[1], self.points[0], self.points[2]) 68 | } 69 | } 70 | 71 | /// Return the circumcenter of the circle that encloses this triangle. 72 | pub fn circumcenter(&self) -> Option> 73 | where 74 | P: ::std::fmt::Debug, 75 | { 76 | let p0p1 = LineEquation::between(&self.points[0], &self.points[1]); 77 | let p0p2 = LineEquation::between(&self.points[0], &self.points[2]); 78 | 79 | let mid_p0p1 = self.points[0].midpoint(&self.points[1]); 80 | let mid_p0p2 = self.points[0].midpoint(&self.points[2]); 81 | 82 | let bisec_p0p1 = p0p1.perpendicular(&mid_p0p1); 83 | let bisec_p0p2 = p0p2.perpendicular(&mid_p0p2); 84 | 85 | bisec_p0p1.intersection(&bisec_p0p2) 86 | } 87 | 88 | /// Return the circumcicle that encloses this triangle as a pair of 89 | /// circumcenter and radius _squared_. 90 | pub fn squared_circumcircle(&self) -> Option<(Point

, O)> 91 | where 92 | O: num::Num + From

+ Copy, 93 | P: ::std::fmt::Debug, 94 | { 95 | self.circumcenter().map(|circumcenter| { 96 | let squared_radius = circumcenter.squared_dist(&self.points[0]); 97 | 98 | (circumcenter, squared_radius) 99 | }) 100 | } 101 | } 102 | 103 | impl

Triangle

104 | where 105 | P: num::Num + num::Signed + From + Copy + PartialOrd, 106 | f64: From

, 107 | { 108 | /// Return the circumcicle that encloses this triangle as a pair of 109 | /// circumcenter and radius. 110 | pub fn circumcircle(&self) -> Option<(Point

, f64)> 111 | where 112 | P: ::std::fmt::Debug, 113 | { 114 | self.circumcenter().map(|circumcenter| { 115 | let radius = circumcenter.dist(&self.points[0]); 116 | 117 | (circumcenter, radius) 118 | }) 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod test { 124 | use super::Triangle; 125 | use geo::PointI32; 126 | 127 | #[test] 128 | fn test_triangle_circumcircle() { 129 | let triangle = Triangle::new( 130 | PointI32::new(3, 2), 131 | PointI32::new(1, 4), 132 | PointI32::new(5, 4), 133 | ); 134 | assert_eq!(triangle.circumcircle(), Some((PointI32::new(3, 4), 2.0))); 135 | 136 | // ensure the algorithm works with vertical lines 137 | let triangle = Triangle::new( 138 | PointI32::new(3, 2), 139 | PointI32::new(5, 4), 140 | PointI32::new(1, 4), 141 | ); 142 | assert_eq!(triangle.circumcircle(), Some((PointI32::new(3, 4), 2.0))); 143 | 144 | let triangle = Triangle::new( 145 | PointI32::new(3, 2), 146 | PointI32::new(5, 2), 147 | PointI32::new(4, 2), 148 | ); 149 | assert_eq!(triangle.circumcircle(), None); 150 | } 151 | 152 | #[test] 153 | fn test_triangle_area() { 154 | let triangle = Triangle::new( 155 | PointI32::new(6, 0), 156 | PointI32::new(0, 0), 157 | PointI32::new(3, 3), 158 | ); 159 | assert_eq!(triangle.area(), 9); 160 | assert_eq!(triangle.signed_area(), -9); 161 | 162 | let triangle = triangle.counter_clockwise(); 163 | assert_eq!(triangle.area(), 9); 164 | assert_eq!(triangle.signed_area(), 9); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /geo/src/utils/ksmallest.rs: -------------------------------------------------------------------------------- 1 | //! This module contains an implementation of 2 | //! [QuickSelect](https://en.wikipedia.org/wiki/Quickselect) to provide O(n) 3 | //! median search. It's an in place selection algorithm that also has the nice 4 | //! side effect of partitioning the data according to the kth element. 5 | 6 | use std; 7 | use std::cmp::Ordering; 8 | 9 | /// Sort the given slice until it finds the kth smallest element. Return None if 10 | /// k is out of bounds. 11 | pub fn ksmallest(elems: &mut [T], k: usize) -> Option<&T> { 12 | ksmallest_by(elems, k, |l, r| l.cmp(r)) 13 | } 14 | 15 | /// Sort the given slice until it finds the kth smallest element according to 16 | /// the given key function. Return None if k is out of bounds. 17 | pub fn ksmallest_by_key(elems: &mut [T], k: usize, mut f: F) -> Option<&T> 18 | where 19 | F: FnMut(&T) -> K, 20 | K: Ord, 21 | { 22 | ksmallest_by(elems, k, |l, r| f(l).cmp(&f(r))) 23 | } 24 | 25 | /// Sort the given slice until it finds the kth smallest element according to 26 | /// the function that returns the ordering between elements. Return None if k is 27 | /// out of bounds. 28 | pub fn ksmallest_by(elems: &mut [T], k: usize, mut f: F) -> Option<&T> 29 | where 30 | F: FnMut(&T, &T) -> Ordering, 31 | { 32 | if k >= elems.len() { 33 | return None; 34 | } 35 | 36 | let mut left = 0; 37 | let mut right = elems.len() - 1; 38 | 39 | loop { 40 | let pivot = partition_by(elems, &mut f, left, right, right); 41 | 42 | match pivot.cmp(&k) { 43 | Ordering::Equal => return Some(&elems[pivot]), 44 | Ordering::Less => left = pivot + 1, 45 | Ordering::Greater => right = pivot - 1, 46 | }; 47 | } 48 | } 49 | 50 | fn partition_by(elems: &mut [T], f: &mut F, left: usize, right: usize, pivot: usize) -> usize 51 | where 52 | F: FnMut(&T, &T) -> Ordering, 53 | { 54 | elems.swap(right, pivot); 55 | let pivot = right; 56 | 57 | let mut store_index = left; 58 | 59 | for i in left..right { 60 | if let Ordering::Less = f(&elems[i], &elems[pivot]) { 61 | elems.swap(store_index, i); 62 | store_index += 1; 63 | } 64 | } 65 | 66 | elems.swap(store_index, pivot); 67 | 68 | store_index 69 | } 70 | 71 | #[cfg(test)] 72 | mod test { 73 | use super::{ksmallest, ksmallest_by}; 74 | 75 | proptest! { 76 | #![proptest_config(proptest::test_runner::Config::with_cases(100))] 77 | #[test] 78 | fn prop_k_smallest(mut v in proptest::collection::vec(0_u8..255, 0..100)) { 79 | let mut sorted = v.clone(); 80 | sorted.sort(); 81 | 82 | for k in 0..v.len() { 83 | assert_eq!(ksmallest(&mut v, k), Some(&sorted[k])); 84 | } 85 | 86 | assert_eq!(sorted, v); 87 | } 88 | } 89 | 90 | proptest! { 91 | #![proptest_config(proptest::test_runner::Config::with_cases(100))] 92 | #[test] 93 | fn prop_k_smallest_by(mut v in proptest::collection::vec(0_u8..255, 0..100)) { 94 | let mut sorted = v.clone(); 95 | sorted.sort(); 96 | sorted.reverse(); 97 | 98 | for k in 0..v.len() { 99 | let v = ksmallest_by(&mut v, k, |l, r| r.cmp(l)); 100 | assert_eq!(v, Some(&sorted[k])); 101 | } 102 | 103 | assert_eq!(sorted, v); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /geo/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! some handy utils. 2 | 3 | use std::cmp::Ordering; 4 | use std::collections::HashMap; 5 | 6 | use std::hash::Hash; 7 | 8 | /// Clamp the given i64 between two u32 inclusive. 9 | pub fn clamp(x: i64, l: u32, h: u32) -> u32 { 10 | x.max(i64::from(l)).min(i64::from(h)) as u32 11 | } 12 | 13 | /// Build a `HashMap` from the keys in the iterator to the number of its 14 | /// occurences. 15 | pub fn build_hashmap_counter(it: I) -> HashMap 16 | where 17 | K: Eq + Hash, 18 | I: Iterator, 19 | { 20 | let mut map = HashMap::new(); 21 | 22 | for k in it { 23 | *map.entry(k).or_insert(0) += 1; 24 | } 25 | 26 | map 27 | } 28 | 29 | /// Split the given vector at the given index and return a vector of all 30 | /// elements before at, the element at the given index and all the elements 31 | /// after. 32 | pub fn split_element_at(mut v: Vec, at: usize) -> (Vec, Option, Vec) { 33 | if v.is_empty() { 34 | return (vec![], None, vec![]); 35 | } 36 | 37 | let right = v.split_off(at + 1); 38 | let elem = v.pop(); 39 | 40 | (v, elem, right) 41 | } 42 | 43 | /// Simple utility to compare f64 taking into account float flakiness. 44 | pub fn cmp_floats>(a1: F, a2: F) -> Ordering { 45 | if a1.is_nan() || a2.is_nan() { 46 | panic!("matto doesn't support nans, that'd be crazy ;)"); 47 | } 48 | 49 | // 1e-10 is a random constant to try to ignore floating points precision 50 | // errors. 51 | if (a2 - a1).abs() < From::from(1e-10) { 52 | return Ordering::Equal; 53 | } 54 | 55 | if a1 < a2 { 56 | Ordering::Less 57 | } else { 58 | Ordering::Greater 59 | } 60 | } 61 | 62 | pub mod ksmallest; 63 | pub mod ordwrapper; 64 | 65 | pub use self::ksmallest::{ksmallest, ksmallest_by, ksmallest_by_key}; 66 | pub use self::ordwrapper::OrdWrapper; 67 | 68 | #[cfg(test)] 69 | mod test { 70 | use super::split_element_at; 71 | 72 | use std::iter; 73 | 74 | proptest! { 75 | #![proptest_config(proptest::test_runner::Config::with_cases(50))] 76 | #[test] 77 | fn prop_split_element_at_built_from_parts( 78 | left in proptest::collection::vec(0_u8..255, 0..100), 79 | elem in (0_u8..), 80 | right in proptest::collection::vec(0_u8..255, 0..100) 81 | ) { 82 | let composed = left 83 | .iter() 84 | .chain(iter::once(&elem)) 85 | .chain(right.iter()) 86 | .cloned() 87 | .collect(); 88 | 89 | let (l, e, r) = split_element_at(composed, left.len()); 90 | 91 | assert_eq!(e, Some(elem)); 92 | assert_eq!(l, left); 93 | assert_eq!(r, right); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /geo/src/utils/ordwrapper.rs: -------------------------------------------------------------------------------- 1 | //! Simple module containing a wrapper struct that allows to sort arbitrary data 2 | //! according to some related orderable value. 3 | 4 | use std::cmp::Ordering; 5 | 6 | /// Struct that allows to order T values using values of Ds as the comparators. 7 | #[derive(Clone, Debug)] 8 | pub struct OrdWrapper { 9 | data: T, 10 | key: K, 11 | } 12 | 13 | impl Into<(T, K)> for OrdWrapper { 14 | fn into(self) -> (T, K) { 15 | (self.data, self.key) 16 | } 17 | } 18 | 19 | impl OrdWrapper { 20 | /// Create a new OrdWrapper with the given data and the given key. 21 | pub fn new(data: T, key: K) -> Self { 22 | OrdWrapper { data, key } 23 | } 24 | 25 | /// Getter for the data. 26 | pub fn data(&self) -> &T { 27 | &self.data 28 | } 29 | 30 | /// Getter for the key. 31 | pub fn key(&self) -> &K { 32 | &self.key 33 | } 34 | } 35 | 36 | impl Eq for OrdWrapper {} 37 | 38 | impl PartialEq for OrdWrapper { 39 | fn eq(&self, other: &Self) -> bool { 40 | self.key == other.key 41 | } 42 | } 43 | 44 | impl PartialOrd for OrdWrapper { 45 | fn partial_cmp(&self, other: &Self) -> Option { 46 | Some(self.cmp(other)) 47 | } 48 | } 49 | 50 | impl Ord for OrdWrapper { 51 | fn cmp(&self, other: &Self) -> Ordering { 52 | self.key.cmp(&other.key) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /images/baboon-quantized.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/baboon-quantized.jpeg -------------------------------------------------------------------------------- /images/baboon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/baboon.jpeg -------------------------------------------------------------------------------- /images/delaunay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/delaunay.png -------------------------------------------------------------------------------- /images/desert-quantized.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/desert-quantized.jpeg -------------------------------------------------------------------------------- /images/desert.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/desert.jpeg -------------------------------------------------------------------------------- /images/fractree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/fractree.png -------------------------------------------------------------------------------- /images/mandelbrot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/mandelbrot.png -------------------------------------------------------------------------------- /images/mondrian1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/mondrian1.png -------------------------------------------------------------------------------- /images/mondrian2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/mondrian2.png -------------------------------------------------------------------------------- /images/patchwork-filled1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/patchwork-filled1.png -------------------------------------------------------------------------------- /images/patchwork-filled2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/patchwork-filled2.png -------------------------------------------------------------------------------- /images/patchwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/patchwork.png -------------------------------------------------------------------------------- /images/rb-primitized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/rb-primitized.png -------------------------------------------------------------------------------- /images/rb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/rb.png -------------------------------------------------------------------------------- /images/red-horns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/red-horns.png -------------------------------------------------------------------------------- /images/redblue-dragon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/redblue-dragon.png -------------------------------------------------------------------------------- /images/runes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/runes.png -------------------------------------------------------------------------------- /images/sierpinski.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/sierpinski.png -------------------------------------------------------------------------------- /images/stippling-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/stippling-gradient.png -------------------------------------------------------------------------------- /images/stippling-rects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/stippling-rects.png -------------------------------------------------------------------------------- /images/tangled-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/tangled-web.png -------------------------------------------------------------------------------- /images/tiffanys-primitized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/tiffanys-primitized.png -------------------------------------------------------------------------------- /images/tiffanys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/tiffanys.jpg -------------------------------------------------------------------------------- /images/voronoi-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/voronoi-gradient.png -------------------------------------------------------------------------------- /images/voronoi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieledapo/mattors/c5a2a8f47d6194d91f9e59a6afcc17f2ed679e8c/images/voronoi.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Matto 8 | 9 | 10 | 11 |

12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /index.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Spectre.css customization 3 | // 4 | 5 | $primary-color: #4d3b51; 6 | 7 | @import "node_modules/spectre.css/src/spectre.scss"; 8 | 9 | .menu-item > a { 10 | padding: 0.05rem 0.4rem; 11 | } 12 | 13 | // 14 | // General 15 | // 16 | 17 | body { 18 | overflow-x: visible; 19 | } 20 | 21 | #piece-canvas-container canvas { 22 | margin: 15px auto; 23 | display: block; 24 | } 25 | -------------------------------------------------------------------------------- /mattopy/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | noise = "*" 8 | 9 | [dev-packages] 10 | black = "==20.8b1" 11 | 12 | [requires] 13 | python_version = "3.6" 14 | -------------------------------------------------------------------------------- /mattopy/mountains.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import math 5 | import random 6 | 7 | import noise 8 | 9 | 10 | THEMES = { 11 | "flame": { 12 | "palette": [ 13 | "rgb(255, 48, 0)", 14 | "rgb(255, 71, 0)", 15 | "rgb(255, 88, 0)", 16 | "rgb(255, 103, 0)", 17 | "rgb(255, 117, 0)", 18 | "rgb(255, 130, 0)", 19 | "rgb(255, 142, 0)", 20 | "rgb(255, 154, 0)", 21 | "rgb(255, 165, 0)", 22 | ], 23 | "background": "rgb(255, 0, 0)", 24 | "connect_bottom": True, 25 | "padding": 0, 26 | "stroke": "none", 27 | }, 28 | "ocean": { 29 | "palette": [ 30 | "rgb(68, 44, 253)", 31 | "rgb(94, 70, 251)", 32 | "rgb(113, 92, 249)", 33 | "rgb(129, 113, 246)", 34 | "rgb(141, 134, 243)", 35 | "rgb(151, 154, 241)", 36 | "rgb(160, 175, 237)", 37 | "rgb(167, 195, 234)", 38 | "rgb(173, 216, 230)", 39 | ], 40 | "background": "rgb(0, 0, 255)", 41 | "connect_bottom": True, 42 | "padding": 0, 43 | "stroke": "none", 44 | }, 45 | "bw": { 46 | "palette": ["none"], 47 | "background": "white", 48 | "connect_bottom": False, 49 | "padding": 0.08, 50 | "stroke": "black", 51 | }, 52 | } 53 | 54 | 55 | def parse_args(): 56 | parser = argparse.ArgumentParser() 57 | parser.add_argument("--width", default=1920, type=int) 58 | parser.add_argument("--height", default=1080, type=int) 59 | parser.add_argument("--points", default=20, type=int) 60 | parser.add_argument("--lines", default=20, type=int) 61 | parser.add_argument("-t", "--theme", choices=list(THEMES.keys()), default="flame") 62 | 63 | return parser.parse_args() 64 | 65 | 66 | def main(): 67 | args = parse_args() 68 | seed = random.random() 69 | 70 | theme = THEMES[args.theme] 71 | palette = theme["palette"] 72 | padding = min(args.width, args.height) * theme["padding"] 73 | 74 | h = args.height - padding * 2 75 | w = args.width - padding * 2 76 | bh = h / args.lines 77 | 78 | segs = [] 79 | for i in range(args.lines): 80 | y = padding + (i + 0.5) / args.lines * h 81 | 82 | def noiseval(x): 83 | return noise.snoise2( 84 | x / (args.points - 1), 85 | i / args.lines, 86 | base=seed, 87 | octaves=4, 88 | lacunarity=2, 89 | persistence=0.9, 90 | ) 91 | 92 | knots = [ 93 | (padding + (x / (args.points - 1) * w), y + bh * 3 * noiseval(x)) 94 | for x in range(args.points) 95 | ] 96 | col = palette[math.floor(i / args.lines * len(palette))] 97 | segs.append((knots, col)) 98 | 99 | dump_svg( 100 | "mountains.svg", 101 | (args.width, args.height), 102 | segs, 103 | background=theme["background"], 104 | stroke=theme["stroke"], 105 | padding=padding, 106 | connect_bottom=theme["connect_bottom"], 107 | ) 108 | 109 | 110 | def dump_svg( 111 | filename, 112 | dimensions, 113 | lines, 114 | background, 115 | stroke="none", 116 | padding=20, 117 | connect_bottom=True, 118 | ): 119 | width, height = dimensions 120 | 121 | def bezier(i, points): 122 | def ctrl_pt(cur, prv, nex, reverse=False, smoothing=0.2): 123 | cur = points[cur] 124 | prv = cur if prv < 0 else points[prv] 125 | nex = cur if nex >= len(points) else points[nex] 126 | 127 | length, angle = line_props(prv, nex) 128 | length *= smoothing 129 | if reverse: 130 | angle += math.pi 131 | 132 | return ( 133 | cur[0] + math.cos(angle) * length, 134 | cur[1] + math.sin(angle) * length, 135 | ) 136 | 137 | cpsx, cpsy = ctrl_pt(i - 1, i - 2, i) 138 | cpex, cpey = ctrl_pt(i, i - 1, i + 1, reverse=True) 139 | return (cpsx, cpsy, cpex, cpey, *points[i]) 140 | 141 | with open(filename, "wt") as fp: 142 | print( 143 | """ 144 | 145 | 146 | 147 | {lines} 148 | """.format( 149 | width=width, 150 | height=height, 151 | background=background, 152 | lines="\n".join( 153 | ''.format( 154 | points=" ".join( 155 | "M {} {}".format(x, y) 156 | if i == 0 157 | else "C {},{} {},{} {},{}".format(*bezier(i, pts)) 158 | for i, (x, y) in enumerate(pts) 159 | ) 160 | + ( 161 | "L {} {} L {} {} ".format( 162 | width - padding, 163 | height - padding, 164 | padding, 165 | height - padding, 166 | ) 167 | if connect_bottom 168 | else "" 169 | ), 170 | fill=fill, 171 | stroke=stroke, 172 | ) 173 | for (pts, fill) in lines 174 | ), 175 | ), 176 | file=fp, 177 | ) 178 | 179 | 180 | def line_props(prv, nex): 181 | lenx = nex[0] - prv[0] 182 | leny = nex[1] - prv[1] 183 | 184 | return (math.sqrt(lenx ** 2 + leny ** 2), math.atan2(leny, lenx)) 185 | 186 | 187 | if __name__ == "__main__": 188 | main() 189 | -------------------------------------------------------------------------------- /mattopy/quads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import math 5 | import random 6 | 7 | import noise 8 | 9 | 10 | THEMES = { 11 | "flame": { 12 | "background": "crimson", 13 | "palette": [ 14 | "rgb(255,0,0)", 15 | "rgb(255,48,0)", 16 | "rgb(255,71,0)", 17 | "rgb(255,88,0)", 18 | "rgb(255,103,0)", 19 | "rgb(255,117,0)", 20 | "rgb(255,130,0)", 21 | "rgb(255,142,0)", 22 | "rgb(255,154,0)", 23 | "rgb(255,165,0)", 24 | ], 25 | "stroke": "none", 26 | }, 27 | "ocean": { 28 | "background": "rgb(0, 0, 101)", 29 | "palette": [ 30 | "rgb(0, 0, 128)", 31 | "rgb(58, 56, 155)", 32 | "rgb(89, 104, 181)", 33 | "rgb(114, 155, 208)", 34 | "rgb(135, 207, 235)", 35 | ], 36 | "stroke": "none", 37 | }, 38 | "acid": { 39 | "background": "rgb(0, 128, 0)", 40 | "palette": [ 41 | "rgb(60, 158, 11)", 42 | "rgb(98, 190, 23)", 43 | "rgb(135, 222, 35)", 44 | "rgb(172, 255, 47)", 45 | ], 46 | "stroke": "none", 47 | }, 48 | "bw": {"background": "white", "palette": ["white"], "stroke": "black"}, 49 | } 50 | 51 | 52 | def parse_args(): 53 | parser = argparse.ArgumentParser() 54 | parser.add_argument("--width", default=1920, type=int) 55 | parser.add_argument("--height", default=1080, type=int) 56 | parser.add_argument("-l", "--quad-size", default=50, type=int) 57 | parser.add_argument("-p", "--particles", default=50, type=int) 58 | parser.add_argument("-s", "--steps", default=50, type=int) 59 | parser.add_argument("-t", "--theme", choices=list(THEMES.keys()), default="flame") 60 | 61 | return parser.parse_args() 62 | 63 | 64 | def main(): 65 | args = parse_args() 66 | padding = 20 67 | seed = random.random() 68 | 69 | quads = [] 70 | for _ in range(args.particles): 71 | x, y = ( 72 | random.randrange(padding, args.width - padding), 73 | random.randrange(padding, args.height - padding), 74 | ) 75 | 76 | for _ in range(args.steps): 77 | z = 0.5 + 0.5 * noise.snoise2(x / args.width, y / args.height, base=seed) 78 | 79 | if random.random() > 0.6: 80 | quads.append(((x, y), z)) 81 | 82 | x += math.cos(z * 2 * math.pi) * 10 83 | y += math.sin(z * 2 * math.pi) * 10 84 | 85 | theme = THEMES[args.theme] 86 | dump_svg( 87 | "quads.svg", 88 | (args.width, args.height), 89 | quads, 90 | padding=padding, 91 | quadl=args.quad_size, 92 | background=theme["background"], 93 | palette=theme["palette"], 94 | stroke=theme["stroke"], 95 | ) 96 | 97 | 98 | def dump_svg( 99 | filename, 100 | dimensions, 101 | quads, 102 | background, 103 | palette, 104 | padding=20, 105 | quadl=50, 106 | stroke="none", 107 | ): 108 | width, height = dimensions 109 | 110 | with open(filename, "wt") as fp: 111 | minl = min(quads, key=lambda q: q[1])[1] 112 | maxl = max(quads, key=lambda q: q[1])[1] 113 | 114 | def l_of(z): 115 | if maxl == minl: 116 | return quadl 117 | 118 | return ((z - minl) / (maxl - minl)) * quadl 119 | 120 | quads = ( 121 | ((cx - l_of(z) / 2, cy - l_of(z) / 2), l_of(z)) for ((cx, cy), z) in quads 122 | ) 123 | 124 | print( 125 | """ 126 | 127 | 128 | 129 | {quads} 130 | """.format( 131 | width=width, 132 | height=height, 133 | background=background, 134 | quads="\n".join( 135 | ''.format( 136 | x, y, l, l, color=random.choice(palette), stroke=stroke 137 | ) 138 | for ((x, y), l) in quads 139 | if x > padding 140 | and x + l < width - padding 141 | and y > padding 142 | and y + l < height - padding 143 | ), 144 | ), 145 | file=fp, 146 | ) 147 | 148 | 149 | if __name__ == "__main__": 150 | main() 151 | -------------------------------------------------------------------------------- /mattors/art/delaunay.rs: -------------------------------------------------------------------------------- 1 | //! Generate some triangly art using Delaunay triangulation. 2 | 3 | use geo::{delaunay, BoundingBox, PointF64, PointU32}; 4 | use rand::Rng; 5 | 6 | use crate::color::{random_color, RandomColorConfig}; 7 | use crate::drawing; 8 | 9 | /// Generate a random triangulation and draws it onto the given image. The 10 | /// points are generated randomly but the image is divided into a grid and each 11 | /// point is contained in a cell. 12 | pub fn random_triangulation( 13 | img: &mut image::RgbaImage, 14 | color_config: &mut RandomColorConfig, 15 | grid_size: u32, 16 | alpha: u8, 17 | ) { 18 | let points = random_points_in_grid(img.width(), img.height(), grid_size); 19 | 20 | let triangles = delaunay::triangulate( 21 | &BoundingBox::from_dimensions(f64::from(img.width()), f64::from(img.height())), 22 | points, 23 | ); 24 | 25 | { 26 | let mut drawer = drawing::Drawer::new_with_no_blending(img); 27 | 28 | for triangle in triangles { 29 | let [ref p1, ref p2, ref p3] = triangle.points; 30 | 31 | let p1 = PointU32::new(p1.x.ceil() as u32, p1.y.ceil() as u32); 32 | let p2 = PointU32::new(p2.x.ceil() as u32, p2.y.ceil() as u32); 33 | let p3 = PointU32::new(p3.x.ceil() as u32, p3.y.ceil() as u32); 34 | 35 | let pix = image::Rgba(random_color(color_config).to_rgba(alpha)); 36 | 37 | drawer.triangle(p1, p2, p3, &pix); 38 | } 39 | } 40 | } 41 | 42 | fn random_points_in_grid(width: u32, height: u32, grid_size: u32) -> Vec { 43 | let mut rng = rand::thread_rng(); 44 | 45 | let square_width = width / grid_size; 46 | let square_height = height / grid_size; 47 | 48 | let mut out = Vec::with_capacity(grid_size as usize * grid_size as usize); 49 | 50 | for xi in 0..grid_size { 51 | for yi in 0..grid_size { 52 | let cur_square_x = f64::from(xi * square_width); 53 | let cur_square_y = f64::from(yi * square_height); 54 | 55 | let x = rng.gen_range( 56 | cur_square_x, 57 | (cur_square_x + f64::from(square_width)).min(f64::from(width)), 58 | ); 59 | let y = rng.gen_range( 60 | cur_square_y, 61 | (cur_square_y + f64::from(square_height)).min(f64::from(height)), 62 | ); 63 | 64 | out.push(PointF64::new(x, y)); 65 | } 66 | } 67 | 68 | out 69 | } 70 | -------------------------------------------------------------------------------- /mattors/art/dithering.rs: -------------------------------------------------------------------------------- 1 | //! Generate some dithered images. 2 | 3 | use image::{GenericImageView, ImageBuffer, Pixel}; 4 | use num::traits::{AsPrimitive, Bounded}; 5 | 6 | /// Perform [Floyd–Steinberg_dithering][0] over a binary image. 7 | /// 8 | /// 0: https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering 9 | pub fn dither( 10 | img: &I, 11 | mut closest: impl FnMut(&I::Pixel) -> I::Pixel, 12 | ) -> ImageBuffer::Subpixel>> 13 | where 14 | I: GenericImageView, 15 | I::Pixel: 'static, 16 | ::Subpixel: 'static, 17 | f32: From<::Subpixel> + AsPrimitive<::Subpixel>, 18 | { 19 | let min_value = ::Subpixel::min_value(); 20 | let max_value = ::Subpixel::max_value(); 21 | 22 | let mut new: ImageBuffer::Subpixel>> = 23 | ImageBuffer::new(img.width(), img.height()); 24 | 25 | for y in 0..new.height() { 26 | for x in 0..new.width() { 27 | let old_pixel = img.get_pixel(x, y); 28 | let new_pixel = closest(&old_pixel); 29 | 30 | let mut err = [0.0; 3]; 31 | for (e, (o, n)) in err 32 | .iter_mut() 33 | .zip(old_pixel.channels().iter().zip(new_pixel.channels())) 34 | { 35 | *e = f32::from(*o) - f32::from(*n); 36 | } 37 | 38 | let mut distribute_err = |(xx, yy), ratio| { 39 | if xx >= new.width() || yy >= new.height() { 40 | return; 41 | } 42 | 43 | let p = new.get_pixel_mut(xx, yy); 44 | for (sp, e) in p.channels_mut().iter_mut().zip(&err) { 45 | let nsp: f32 = f32::from(*sp) + e * ratio; 46 | 47 | *sp = nsp 48 | .max(f32::from(min_value)) 49 | .min(f32::from(max_value)) 50 | .as_(); 51 | } 52 | }; 53 | 54 | distribute_err((x + 1, y), 7.0 / 16.0); 55 | 56 | if x > 0 { 57 | distribute_err((x - 1, y + 1), 3.0 / 16.0); 58 | } 59 | 60 | distribute_err((x, y + 1), 5.0 / 16.0); 61 | distribute_err((x + 1, y + 1), 5.0 / 16.0); 62 | 63 | new.put_pixel(x, y, new_pixel); 64 | } 65 | } 66 | 67 | new 68 | } 69 | -------------------------------------------------------------------------------- /mattors/art/dragon.rs: -------------------------------------------------------------------------------- 1 | //! Simple module to draw some [Dragon 2 | //! Curves](https://en.wikipedia.org/wiki/Dragon_curve). 3 | 4 | use geo::PointU32; 5 | 6 | use crate::drawing; 7 | 8 | /// A move the Dragon Fractal can take 9 | #[derive(Clone, Debug)] 10 | pub enum Move { 11 | /// Go down 12 | Down, 13 | 14 | /// Go left 15 | Left, 16 | 17 | /// Go right 18 | Right, 19 | 20 | /// Go up 21 | Up, 22 | } 23 | 24 | impl Move { 25 | /// return the move that is obtained by rotating the current move in 26 | /// clockwise order. 27 | pub fn clockwise(&self) -> Move { 28 | match *self { 29 | Move::Down => Move::Left, 30 | Move::Left => Move::Up, 31 | Move::Right => Move::Down, 32 | Move::Up => Move::Right, 33 | } 34 | } 35 | } 36 | 37 | /// A [Dragon Fractal](https://en.wikipedia.org/wiki/Dragon_curve). 38 | #[derive(Debug)] 39 | pub struct Dragon(Vec); 40 | 41 | /// Generate a [Dragon Fractal](https://en.wikipedia.org/wiki/Dragon_curve) from 42 | /// an `initial` move iterating `n` times. 43 | pub fn dragon(n: u32, initial: Move) -> Dragon { 44 | let mut moves = Vec::with_capacity(2_usize.pow(n)); 45 | moves.push(initial); 46 | 47 | for _ in 0..n { 48 | let cur_len = moves.len(); 49 | 50 | for i in 0..cur_len { 51 | let mv = moves[cur_len - i - 1].clockwise(); 52 | moves.push(mv); 53 | } 54 | } 55 | 56 | Dragon(moves) 57 | } 58 | 59 | /// Generate a Fractal(I think) based on the same process as the `dragon`. The 60 | /// difference is that the new move is calculated not from the last move, but 61 | /// the first one. 62 | pub fn horns(n: u32, initial: Move) -> Dragon { 63 | let mut moves = Vec::with_capacity(2_usize.pow(n)); 64 | moves.push(initial); 65 | 66 | for _ in 0..n { 67 | let cur_len = moves.len(); 68 | 69 | for i in 0..cur_len { 70 | let mv = moves[i].clockwise(); 71 | moves.push(mv); 72 | } 73 | } 74 | 75 | Dragon(moves) 76 | } 77 | 78 | /// Generate a [Dragon Fractal](https://en.wikipedia.org/wiki/Dragon_curve) and 79 | /// dump it to an image with the given color. 80 | pub fn dragon_to_image( 81 | drag: &Dragon, 82 | width: u32, 83 | height: u32, 84 | start_x: u32, 85 | start_y: u32, 86 | line_len: u32, 87 | rgb_color: [u8; 3], 88 | ) -> image::RgbImage { 89 | // TODO: might be interesting to add [perlin 90 | // noise](https://en.wikipedia.org/wiki/Perlin_noise) 91 | let mut img = image::ImageBuffer::new(width, height); 92 | 93 | { 94 | let mut drawer = drawing::Drawer::new_with_no_blending(&mut img); 95 | 96 | let pix = image::Rgb(rgb_color); 97 | 98 | let mut x = start_x; 99 | let mut y = start_y; 100 | 101 | for m in &drag.0 { 102 | let (nx, ny) = { 103 | match *m { 104 | Move::Down => (x, y.saturating_add(line_len)), 105 | Move::Left => (x.saturating_sub(line_len), y), 106 | Move::Right => (x.saturating_add(line_len), y), 107 | Move::Up => (x, y.saturating_sub(line_len)), 108 | } 109 | }; 110 | 111 | drawer.line(PointU32::new(x, y), PointU32::new(nx, ny), &pix); 112 | 113 | x = nx; 114 | y = ny; 115 | } 116 | } 117 | 118 | img 119 | } 120 | -------------------------------------------------------------------------------- /mattors/art/fractree.rs: -------------------------------------------------------------------------------- 1 | //! Generate some awesome Fractal Trees. 2 | 3 | use std::f64; 4 | use std::fmt::Debug; 5 | 6 | use geo::PointU32; 7 | 8 | use crate::drawing; 9 | 10 | /// Draw a fractal tree onto the given `img` using the given `pix` starting from 11 | /// `pt`. `branching_angle` is the angle to use to draw the branches and 12 | /// `branch_len` is the branch length. `branch_angle_step` is an angle that is 13 | /// added and subtracted from `angle` to move branches. `branch_len_factor` is 14 | /// multiplied with `branch_len` to change the `branch_len`. 15 | #[allow(clippy::too_many_arguments)] 16 | pub fn fractal_tree( 17 | img: &mut I, 18 | nbranches: u32, 19 | pt: PointU32, 20 | branching_angle: f64, 21 | branching_angle_step: f64, 22 | branch_len: f64, 23 | branch_len_factor: f64, 24 | pix: &I::Pixel, 25 | ) where 26 | I: image::GenericImage, 27 | I::Pixel: Debug, 28 | f64: From<::Subpixel>, 29 | { 30 | if nbranches == 0 { 31 | return; 32 | } 33 | 34 | let breakpoint = { 35 | let x = 36 | (>::from(pt.x) + branching_angle.cos() * branch_len).max(0.0) as u32; 37 | let y = 38 | (>::from(pt.y) + branching_angle.sin() * branch_len).max(0.0) as u32; 39 | 40 | PointU32::new(x, y) 41 | }; 42 | 43 | { 44 | let mut drawer = drawing::Drawer::new_with_no_blending(img); 45 | drawer.antialiased_line(pt, breakpoint, pix); 46 | } 47 | 48 | fractal_tree( 49 | img, 50 | nbranches - 1, 51 | breakpoint, 52 | branching_angle + branching_angle_step, 53 | branching_angle_step, 54 | branch_len * branch_len_factor, 55 | branch_len_factor, 56 | pix, 57 | ); 58 | 59 | fractal_tree( 60 | img, 61 | nbranches - 1, 62 | breakpoint, 63 | branching_angle - branching_angle_step, 64 | branching_angle_step, 65 | branch_len * branch_len_factor, 66 | branch_len_factor, 67 | pix, 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /mattors/art/julia.rs: -------------------------------------------------------------------------------- 1 | //! Simple module to draw some [Julia 2 | //! Set](https://en.wikipedia.org/wiki/Julia_set). The most famous one is 3 | //! probably the [Mandelbrot Set](https://en.wikipedia.org/wiki/Mandelbrot_set). 4 | 5 | use std::iter::Iterator; 6 | 7 | use num::complex::Complex64; 8 | 9 | use geo::PointF64; 10 | 11 | /// This struct is mainly used to pass some data used when converting to raw 12 | /// pixels. 13 | #[derive(Debug)] 14 | pub struct FractalPoint { 15 | is_inside: bool, 16 | last_value: f64, 17 | iterations: u32, 18 | } 19 | 20 | impl FractalPoint { 21 | /// Calculate if the given `f`(that is point) is in the [Mandelbrot 22 | /// Set](https://en.wikipedia.org/wiki/Mandelbrot_set). 23 | pub fn mandelbrot(f: Complex64, iterations: u32) -> FractalPoint { 24 | FractalPoint::julia(f, f, iterations) 25 | } 26 | 27 | /// Calculate if the given `f`(that is point) with param `c` is in the 28 | /// [Julia Set](https://en.wikipedia.org/wiki/Julia_set). `iterations` is 29 | /// the maximum number of times this function can perform the check to see 30 | /// whether a given point is inside the set or not. 31 | pub fn julia(mut f: Complex64, c: Complex64, iterations: u32) -> FractalPoint { 32 | let mut is_inside = true; 33 | let mut i = 0; 34 | 35 | while i < iterations { 36 | f = f * f + c; 37 | 38 | if f.norm() > 2.0 { 39 | is_inside = false; 40 | break; 41 | } 42 | 43 | i += 1; 44 | } 45 | 46 | FractalPoint { 47 | last_value: f.norm(), 48 | iterations: i, 49 | is_inside, 50 | } 51 | } 52 | 53 | fn to_pixels(&self) -> Vec { 54 | if self.is_inside { 55 | vec![ 56 | 0, 57 | (self.last_value * 128.0) as u8, 58 | ((2.0 - self.last_value) * 100.0) as u8, 59 | ] 60 | 61 | //let last_value = (self.last_value * 1_000_000.0) as u32; 62 | // vec![0, (last_value % 255) as u8, (last_value % 255) as u8] 63 | } else { 64 | vec![ 65 | (self.iterations >> 16) as u8, 66 | (self.iterations >> 8) as u8, 67 | self.iterations as u8, 68 | ] 69 | } 70 | } 71 | } 72 | 73 | /// Iterator that returns all the `FractalPoint` 74 | pub struct JuliaGenIter FractalPoint> { 75 | // params 76 | start: PointF64, 77 | xcount: u32, 78 | ycount: u32, 79 | stepx: f64, 80 | stepy: f64, 81 | iterations: u32, 82 | gen_fn: F, 83 | 84 | // state 85 | x: u32, 86 | y: u32, 87 | } 88 | 89 | impl FractalPoint> JuliaGenIter { 90 | /// Create a new `JuliaGenIter` that returns all the `FractalPoint`s from 91 | /// `start` moving x by `stepx` `xcount` times and y by `stepy` `ycount` 92 | /// times. Both `ycount` and `xcount` are exclusive. `gen_fn` is the 93 | /// generator function that takes the current position as a complex number 94 | /// and that returns the `FractalPoint`. 95 | pub fn new( 96 | start: PointF64, 97 | xcount: u32, 98 | ycount: u32, 99 | stepx: f64, 100 | stepy: f64, 101 | iterations: u32, 102 | gen_fn: F, 103 | ) -> JuliaGenIter { 104 | JuliaGenIter { 105 | start, 106 | xcount, 107 | ycount, 108 | stepx, 109 | stepy, 110 | iterations, 111 | gen_fn, 112 | x: 0, 113 | y: 0, 114 | } 115 | } 116 | 117 | /// Consume the `JuliaGenIter` and return an image of the Julia set formed 118 | /// by all the points this iterator yields. 119 | pub fn into_image(self) -> Option, Vec>> { 120 | let width = self.xcount; 121 | let height = self.ycount; 122 | 123 | image::ImageBuffer::from_raw(width, height, self.flat_map(|pt| pt.to_pixels()).collect()) 124 | } 125 | } 126 | 127 | impl FractalPoint> Iterator for JuliaGenIter { 128 | type Item = FractalPoint; 129 | 130 | fn next(&mut self) -> Option { 131 | if self.y >= self.ycount { 132 | return None; 133 | } 134 | 135 | let x = self.start.x + f64::from(self.x) * self.stepx; 136 | let y = self.start.y + f64::from(self.y) * self.stepy; 137 | 138 | let pt = (self.gen_fn)(Complex64::new(x, y), self.iterations); 139 | 140 | self.x += 1; 141 | if self.x >= self.xcount { 142 | self.x = 0; 143 | self.y += 1; 144 | } 145 | 146 | Some(pt) 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::*; 153 | 154 | #[test] 155 | fn sanity() { 156 | assert_eq!( 157 | FractalPoint::mandelbrot(Complex64::new(0.0, 0.0), 128).is_inside, 158 | true 159 | ); 160 | assert_eq!( 161 | FractalPoint::mandelbrot(Complex64::new(-1.0, 0.0), 64).is_inside, 162 | true 163 | ); 164 | assert_eq!( 165 | FractalPoint::mandelbrot(Complex64::new(1.0, 0.0), 12).is_inside, 166 | false 167 | ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /mattors/art/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the code to generate the images 2 | 3 | use std::collections::HashSet; 4 | 5 | use rand::Rng; 6 | 7 | use geo::{BoundingBox, PointU32}; 8 | 9 | /// Randomly subdive the given box in at most n subboxes of at least 10 | /// minimum_area. 11 | pub fn random_bbox_subdivisions( 12 | n: usize, 13 | bbox: BoundingBox, 14 | minimum_area: u32, 15 | rng: &mut R, 16 | ) -> impl Iterator> { 17 | let mut small_bboxes = vec![]; 18 | let mut boxes = vec![bbox]; 19 | 20 | for _ in 0..n { 21 | if boxes.is_empty() { 22 | break; 23 | } 24 | 25 | let i = rng.gen_range(0, boxes.len()); 26 | let b = boxes.swap_remove(i); 27 | 28 | // if either the x or y coordinates are the same then we cannot get a 29 | // random number because it wouldn't make sense. Just skip the item. 30 | if b.min().x == b.max().x || b.min().y == b.max().y { 31 | continue; 32 | } 33 | 34 | let random_point = random_point_in_bbox(rng, &b); 35 | let sub_boxes = b.split_at(&random_point).unwrap(); 36 | 37 | let mut add_piece = |p: BoundingBox| { 38 | if p.area().unwrap() <= minimum_area { 39 | small_bboxes.push(p); 40 | } else { 41 | boxes.push(p); 42 | } 43 | }; 44 | 45 | add_piece(sub_boxes.0); 46 | add_piece(sub_boxes.1); 47 | add_piece(sub_boxes.2); 48 | add_piece(sub_boxes.3); 49 | } 50 | 51 | boxes.into_iter().chain(small_bboxes) 52 | } 53 | 54 | /// Generate n distinct random points in bbox. 55 | pub fn generate_distinct_random_points( 56 | rng: &mut R, 57 | n: usize, 58 | bbox: &BoundingBox, 59 | ) -> HashSet { 60 | let mut points = HashSet::new(); 61 | 62 | // TODO: if n > number of points in bbox panic! 63 | // TODO: if n is high it's probably faster to generate all the points and 64 | // shuffle the array. 65 | while points.len() < n { 66 | points.insert(random_point_in_bbox(rng, bbox)); 67 | } 68 | 69 | points 70 | } 71 | 72 | /// Generate a random point in a bbox. 73 | pub fn random_point_in_bbox(rng: &mut R, bbox: &BoundingBox) -> PointU32 { 74 | let x = rng.gen_range(bbox.min().x, bbox.max().x); 75 | let y = rng.gen_range(bbox.min().y, bbox.max().y); 76 | 77 | PointU32::new(x, y) 78 | } 79 | 80 | pub mod delaunay; 81 | pub mod dithering; 82 | pub mod dragon; 83 | pub mod fractree; 84 | pub mod julia; 85 | pub mod mondrian; 86 | pub mod patchwork; 87 | pub mod primi; 88 | pub mod quantize; 89 | pub mod runes; 90 | pub mod sierpinski; 91 | pub mod stippling; 92 | pub mod tangled_web; 93 | pub mod voronoi; 94 | -------------------------------------------------------------------------------- /mattors/art/mondrian.rs: -------------------------------------------------------------------------------- 1 | //! Generate some art inspired by `Composition in Red, Blue and Yellow` by 2 | //! [Mondrian](https://en.wikipedia.org/wiki/Piet_Mondrian). 3 | 4 | use rand::Rng; 5 | 6 | use geo::{utils::clamp, BoundingBox, PointU32}; 7 | 8 | use crate::art::random_bbox_subdivisions; 9 | use crate::drawing::{Drawer, NoopBlender}; 10 | 11 | /// Generate some Mondrian inspired artwork. 12 | pub fn generate( 13 | img: &mut image::RgbImage, 14 | iterations: usize, 15 | minimum_area: u32, 16 | white: image::Rgb, 17 | fill_palette: &[image::Rgb], 18 | border_thickness: u32, 19 | ) { 20 | let mut rng = rand::thread_rng(); 21 | 22 | let mut drawer = Drawer::new_with_no_blending(img); 23 | 24 | let (width, height) = drawer.dimensions(); 25 | 26 | let rects = random_bbox_subdivisions( 27 | iterations, 28 | BoundingBox::from_dimensions(width, height), 29 | minimum_area, 30 | &mut rng, 31 | ) 32 | .collect::>(); 33 | 34 | let mut draw_rect = |rect, pix| { 35 | drawer.rect(rect, &pix); 36 | draw_borders(&mut drawer, rect, border_thickness); 37 | }; 38 | 39 | for rect in &rects { 40 | draw_rect(rect, white); 41 | } 42 | 43 | if !rects.is_empty() { 44 | let k = rng.gen_range(0, fill_palette.len() + 1); 45 | 46 | for pix in &fill_palette[..k] { 47 | let r = rng.gen_range(0, rects.len()); 48 | 49 | draw_rect(&rects[r], *pix); 50 | } 51 | } 52 | } 53 | 54 | // TODO: drawing borders should be done by the drawing mod. 55 | fn draw_borders( 56 | drawer: &mut Drawer, 57 | rect: &BoundingBox, 58 | border_thickness: u32, 59 | ) { 60 | let (width, height) = drawer.dimensions(); 61 | 62 | let horizontal_band_width = rect.width().unwrap(); 63 | let vertical_band_height = clamp( 64 | i64::from(rect.height().unwrap()) - i64::from(border_thickness) * 2, 65 | 0, 66 | height, 67 | ); 68 | 69 | let borders = [ 70 | BoundingBox::from_dimensions_and_origin( 71 | rect.min(), 72 | horizontal_band_width, 73 | border_thickness, 74 | ), 75 | BoundingBox::from_dimensions_and_origin( 76 | &PointU32::new(rect.min().x, rect.min().y + border_thickness), 77 | border_thickness, 78 | vertical_band_height, 79 | ), 80 | BoundingBox::from_dimensions_and_origin( 81 | &PointU32::new( 82 | clamp( 83 | i64::from(rect.max().x) - i64::from(border_thickness), 84 | 0, 85 | width, 86 | ), 87 | rect.min().y + border_thickness, 88 | ), 89 | border_thickness, 90 | vertical_band_height, 91 | ), 92 | BoundingBox::from_dimensions_and_origin( 93 | &PointU32::new( 94 | rect.min().x, 95 | clamp( 96 | i64::from(rect.max().y) - i64::from(border_thickness), 97 | 0, 98 | height, 99 | ), 100 | ), 101 | horizontal_band_width, 102 | border_thickness, 103 | ), 104 | ]; 105 | 106 | for border in &borders { 107 | drawer.rect(border, &image::Rgb([0, 0, 0])); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /mattors/art/patchwork.rs: -------------------------------------------------------------------------------- 1 | //! Some art inspired by the [Patchwork 2 | //! algorithm](https://mattdesl.svbtle.com/pen-plotter-2). 3 | 4 | use std::collections::HashSet; 5 | 6 | use geo::{convex_hull, kmeans, BoundingBox, Point, Polygon}; 7 | 8 | use crate::art::random_point_in_bbox; 9 | use crate::drawing::{Blender, Drawer}; 10 | 11 | const WHITE_EGG: image::Rgb = image::Rgb([0xFD, 0xFD, 0xFF]); 12 | const BLACK_MATTERHORN: image::Rgb = image::Rgb([0x52, 0x4B, 0x4B]); 13 | 14 | /// Generate random shapes according to the PatchWork algorithm. 15 | pub fn random_patchwork( 16 | img: &mut image::RgbImage, 17 | npoints: usize, 18 | k: usize, 19 | iterations: usize, 20 | fill_polygons: bool, 21 | ) { 22 | let mut generations = vec![vec![Polygon::new(vec![ 23 | Point::new(0.0, 0.0), 24 | Point::new(f64::from(img.width() - 1), 0.0), 25 | Point::new(f64::from(img.width() - 1), f64::from(img.height() - 1)), 26 | Point::new(0.0, f64::from(img.height() - 1)), 27 | ]) 28 | .unwrap()]]; 29 | 30 | let mut drawer = Drawer::new_with_no_blending(img); 31 | 32 | drawer.fill(&WHITE_EGG); 33 | 34 | let mut i = 0; 35 | 36 | while let Some(polygons) = generations.pop() { 37 | if i >= iterations { 38 | if fill_polygons { 39 | for poly in polygons { 40 | let poly = 41 | Polygon::new(poly.points().iter().map(|p| p.try_cast().unwrap())).unwrap(); 42 | 43 | drawer.polygon(&poly, &BLACK_MATTERHORN); 44 | } 45 | } 46 | 47 | break; 48 | } 49 | 50 | i += 1; 51 | 52 | let new_polygons = polygons 53 | .iter() 54 | .flat_map(|poly| patchwork_step(&mut drawer, &poly, npoints, k, !fill_polygons)) 55 | .collect::>(); 56 | 57 | if !new_polygons.is_empty() { 58 | generations.push(new_polygons); 59 | } 60 | } 61 | } 62 | 63 | fn patchwork_step>>( 64 | drawer: &mut Drawer, 65 | polygon: &Polygon, 66 | npoints: usize, 67 | k: usize, 68 | draw_polygons_boundary: bool, 69 | ) -> Vec> { 70 | let mut rng = rand::thread_rng(); 71 | 72 | let polygon_bbox = BoundingBox::from_points(&[ 73 | polygon.bounding_box().min().try_cast().unwrap(), 74 | polygon.bounding_box().max().try_cast().unwrap(), 75 | ]); 76 | 77 | let mut points = (0..npoints) 78 | .map(|_| random_point_in_bbox(&mut rng, &polygon_bbox)) 79 | .collect::>(); 80 | 81 | points.retain(|pt| polygon.contains(&pt.cast())); 82 | 83 | if points.len() <= 2 { 84 | return vec![]; 85 | } 86 | 87 | let mut polygons = vec![]; 88 | 89 | loop { 90 | let clusters = kmeans::kmeans(points.iter().map(|p| p.cast::()), k, 300); 91 | 92 | let smallest_cluster = clusters 93 | .iter() 94 | .filter(|(_, cluster)| cluster.len() > 2) 95 | .min_by_key(|(_, cluster)| cluster.len()); 96 | 97 | match smallest_cluster { 98 | None => break, 99 | Some((_pivot, cluster)) => { 100 | let hull = convex_hull::convex_hull( 101 | cluster.iter().map(|p| p.try_cast::().unwrap().cast()), 102 | ); 103 | 104 | for pt in cluster { 105 | points.remove(&pt.try_cast().unwrap()); 106 | } 107 | 108 | if draw_polygons_boundary { 109 | drawer.closed_path( 110 | hull.iter().map(|p| p.try_cast().unwrap()), 111 | &BLACK_MATTERHORN, 112 | ); 113 | } 114 | 115 | if let Some(new_poly) = Polygon::new(hull) { 116 | polygons.push(new_poly); 117 | } 118 | } 119 | } 120 | } 121 | 122 | polygons 123 | } 124 | -------------------------------------------------------------------------------- /mattors/art/runes.rs: -------------------------------------------------------------------------------- 1 | //! Simple module to generate some rune like characters 2 | 3 | use std::fmt::Debug; 4 | 5 | use rand::prelude::*; 6 | 7 | use geo::PointU32; 8 | 9 | use crate::drawing::Drawer; 10 | 11 | #[derive(Debug)] 12 | enum Simmetry { 13 | Horizontal, 14 | None, 15 | Vertical, 16 | VerticalAndHorizontal, 17 | } 18 | 19 | impl Simmetry { 20 | fn random<'a, R: Rng>(rng: &mut R) -> &'a Self { 21 | [ 22 | Simmetry::Horizontal, 23 | Simmetry::None, 24 | Simmetry::Vertical, 25 | Simmetry::VerticalAndHorizontal, 26 | ] 27 | .choose(rng) 28 | .unwrap() 29 | } 30 | 31 | fn divide(&self, width: u32, height: u32) -> (u32, u32) { 32 | match *self { 33 | Simmetry::Horizontal => (width, height / 2), 34 | Simmetry::None => (width, height), 35 | Simmetry::Vertical => (width / 2, height), 36 | Simmetry::VerticalAndHorizontal => (width / 2, height / 2), 37 | } 38 | } 39 | 40 | fn mirror_image( 41 | &self, 42 | img: &mut I, 43 | simmetry_dimensions: (u32, u32), 44 | img_dimensions: (u32, u32), 45 | ) { 46 | let (simmetry_width, simmetry_height) = simmetry_dimensions; 47 | let (width, height) = img_dimensions; 48 | 49 | match *self { 50 | Simmetry::None => {} 51 | Simmetry::Horizontal => { 52 | for y in 0..simmetry_height { 53 | for x in 0..simmetry_width { 54 | let p = img.get_pixel(x, y); 55 | img.put_pixel(x, height - y - 1, p); 56 | } 57 | } 58 | } 59 | Simmetry::Vertical => { 60 | for y in 0..simmetry_height { 61 | for x in 0..simmetry_width { 62 | let p = img.get_pixel(x, y); 63 | img.put_pixel(width - x - 1, y, p); 64 | } 65 | } 66 | } 67 | Simmetry::VerticalAndHorizontal => { 68 | for y in 0..simmetry_height { 69 | for x in 0..simmetry_width { 70 | let p = img.get_pixel(x, y); 71 | 72 | img.put_pixel(width - x - 1, y, p); 73 | img.put_pixel(x, height - y - 1, p); 74 | img.put_pixel(width - x - 1, height - y - 1, p); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | /// Draw a rune like shape onto the given image. 83 | pub fn draw_random_rune(rune: &mut I, npoints: u32, fg_color: &I::Pixel) 84 | where 85 | I: image::GenericImage, 86 | I::Pixel: Debug, 87 | f64: From<::Subpixel>, 88 | { 89 | let mut rng = rand::thread_rng(); 90 | 91 | let simmetry = Simmetry::random(&mut rng); 92 | 93 | let (rune_width, rune_height) = rune.dimensions(); 94 | let (rune_quad_width, rune_quad_height) = simmetry.divide(rune_width, rune_height); 95 | 96 | { 97 | let mut quad = rune.sub_image(0, 0, rune_quad_width, rune_quad_height); 98 | draw_random_rune_quad(&mut quad, npoints, fg_color, &mut rng); 99 | } 100 | 101 | simmetry.mirror_image( 102 | rune, 103 | (rune_quad_width, rune_quad_height), 104 | (rune_width, rune_height), 105 | ); 106 | 107 | if rng.gen_bool(0.75) { 108 | let mut drawer = Drawer::new_with_no_blending(rune); 109 | 110 | let mw = rune_width / 2; 111 | drawer.line( 112 | PointU32::new(mw, 0), 113 | PointU32::new(mw, rune_height), 114 | fg_color, 115 | ); 116 | } 117 | } 118 | 119 | fn draw_random_rune_quad(quad: &mut I, npoints: u32, fg_color: &I::Pixel, rng: &mut R) 120 | where 121 | I: image::GenericImage, 122 | I::Pixel: Debug, 123 | R: Rng, 124 | f64: From<::Subpixel>, 125 | { 126 | let mut drawer = Drawer::new_with_no_blending(quad); 127 | 128 | let (quad_width, quad_height) = drawer.dimensions(); 129 | let mw = quad_width / 2; 130 | 131 | (0..npoints).fold( 132 | PointU32::new(mw, rng.gen_range(0, quad_height)), 133 | |last_point, _| { 134 | let x = rng.gen_range(mw, quad_width); 135 | let y = rng.gen_range(0, quad_height); 136 | 137 | let p = PointU32::new(x, y); 138 | drawer.antialiased_line(last_point, p, fg_color); 139 | 140 | p 141 | }, 142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /mattors/art/sierpinski.rs: -------------------------------------------------------------------------------- 1 | //! Generate sierpinski triangle 2 | 3 | use std::fmt::Debug; 4 | use std::iter::Iterator; 5 | 6 | use rand::prelude::*; 7 | 8 | use geo::PointU32; 9 | 10 | use crate::drawing; 11 | 12 | /// Handy alias for a [Sierpinski 13 | /// Triangle](https://en.wikipedia.org/wiki/Sierpinski_triangle). Order is top, 14 | /// left and right. 15 | pub type SierpinskiTriangle = (PointU32, PointU32, PointU32); 16 | 17 | /// Iterator over all the iterations of the [Sierpinski 18 | /// Triangle](https://en.wikipedia.org/wiki/Sierpinski_triangle) 19 | pub struct SierpinskiIter { 20 | triangles: Vec, 21 | } 22 | 23 | impl SierpinskiIter { 24 | /// Create a new `SierpinskiIter` bound by the origin and the given `width` 25 | /// and `height`. 26 | pub fn new(width: u32, height: u32) -> SierpinskiIter { 27 | let initial_triangle = ( 28 | PointU32::new(width / 2, 0), 29 | PointU32::new(0, height - 1), 30 | PointU32::new(width - 1, height - 1), 31 | ); 32 | 33 | SierpinskiIter { 34 | triangles: vec![initial_triangle], 35 | } 36 | } 37 | } 38 | 39 | impl Iterator for SierpinskiIter { 40 | type Item = Vec; 41 | 42 | fn next(&mut self) -> Option { 43 | use std::mem::replace; 44 | 45 | let old_triangles = replace(&mut self.triangles, vec![]); 46 | 47 | self.triangles.extend( 48 | old_triangles 49 | .iter() 50 | .flat_map(|&(ref top, ref left, ref right)| { 51 | let mid_left = 52 | PointU32::new(top.x - (top.x - left.x) / 2, top.y + (left.y - top.y) / 2); 53 | let mid_right = PointU32::new(top.x + (top.x - left.x) / 2, mid_left.y); 54 | let mid_bottom = PointU32::new(top.x, left.y); 55 | 56 | let new_top = (*top, mid_left, mid_right); 57 | let new_left = (mid_left, *left, mid_bottom); 58 | let new_right = (mid_right, mid_bottom, *right); 59 | 60 | vec![new_top, new_left, new_right].into_iter() 61 | }), 62 | ); 63 | 64 | Some(old_triangles) 65 | } 66 | } 67 | 68 | /// Draw a fancy [Sierpinski 69 | /// Triangle](https://en.wikipedia.org/wiki/Sierpinski_triangle) on the given 70 | /// image. 71 | pub fn fancy_sierpinski( 72 | img: &mut I, 73 | iterations: usize, 74 | hollow_triangles: bool, 75 | pixs: &[I::Pixel], 76 | ) where 77 | I: image::GenericImage, 78 | I::Pixel: Debug, 79 | { 80 | if pixs.is_empty() { 81 | return; 82 | } 83 | 84 | let mut rng = rand::thread_rng(); 85 | 86 | let (width, height) = img.dimensions(); 87 | let mut siter = SierpinskiIter::new(width, height); 88 | 89 | let mut drawer = drawing::Drawer::new_with_no_blending(img); 90 | 91 | siter 92 | .next() 93 | .map(|triangles| { 94 | drawer.hollow_triangle( 95 | triangles[0].0, 96 | triangles[0].1, 97 | triangles[0].2, 98 | pixs.choose(&mut rng).unwrap(), 99 | ); 100 | 101 | siter.take(iterations).for_each(|triangles| { 102 | triangles 103 | .iter() 104 | .for_each(|&(ref mid_left, ref mid_right, ref mid_bottom)| { 105 | let pix = pixs.choose(&mut rng).unwrap(); 106 | 107 | if hollow_triangles { 108 | drawer.hollow_triangle(*mid_left, *mid_right, *mid_bottom, pix); 109 | } else { 110 | drawer.triangle(*mid_left, *mid_right, *mid_bottom, pix); 111 | } 112 | }); 113 | }); 114 | }) 115 | .unwrap(); 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use super::*; 121 | 122 | #[test] 123 | fn test_sierpinski_iter() { 124 | assert_eq!( 125 | SierpinskiIter::new(101, 101).take(2).collect::>(), 126 | vec![ 127 | vec![( 128 | PointU32::new(50, 0), 129 | PointU32::new(0, 100), 130 | PointU32::new(100, 100), 131 | )], 132 | vec![ 133 | ( 134 | PointU32::new(50, 0), 135 | PointU32::new(25, 50), 136 | PointU32::new(75, 50), 137 | ), 138 | ( 139 | PointU32::new(25, 50), 140 | PointU32::new(0, 100), 141 | PointU32::new(50, 100), 142 | ), 143 | ( 144 | PointU32::new(75, 50), 145 | PointU32::new(50, 100), 146 | PointU32::new(100, 100), 147 | ), 148 | ], 149 | ], 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /mattors/art/stippling.rs: -------------------------------------------------------------------------------- 1 | //! Generate some stippling art. 2 | 3 | use geo::{BoundingBox, PointU32}; 4 | 5 | use crate::art::{random_bbox_subdivisions, random_point_in_bbox}; 6 | use crate::drawing::{Drawer, NoopBlender}; 7 | 8 | /// The direction of gradient made of stippled points. 9 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 10 | pub enum Direction { 11 | /// The gradient is generated from left to right. 12 | LeftToRight, 13 | 14 | /// The gradient is generated from right to left. 15 | RightToLeft, 16 | 17 | /// The gradient is generated from top to bottom. 18 | TopToBottom, 19 | 20 | /// The gradient is generated from bottom to top. 21 | BottomToTop, 22 | } 23 | 24 | /// Stipple the given image in bands with increasing number of points to 25 | /// simulate a gradient. Inspired by http://www.tylerlhobbs.com/works/series/st. 26 | pub fn gradient( 27 | img: &mut image::RgbImage, 28 | bands: u32, 29 | base_points_per_band: u32, 30 | grow_coeff: u32, 31 | pix: image::Rgb, 32 | dir: Direction, 33 | ) { 34 | let mut band = initial_band(dir, img.width(), img.height(), bands); 35 | let mut band_npoints = base_points_per_band; 36 | 37 | let mut drawer = Drawer::new_with_no_blending(img); 38 | 39 | for i in 0..bands { 40 | stipple(&mut drawer, &band, band_npoints, pix); 41 | 42 | // prevent overflow when dir is either RightToLeft or BottomToTop, 43 | // because at the (bands - 1)-th iteration we reached x = 0 or y = 0 and 44 | // we cannot advance anymore. 45 | if i == bands - 1 { 46 | continue; 47 | } 48 | 49 | band = advance_band(&band, dir); 50 | band_npoints += band_npoints * grow_coeff; 51 | } 52 | } 53 | 54 | /// Stipple random rectangles. 55 | pub fn rects( 56 | img: &mut image::RgbImage, 57 | iterations: usize, 58 | points: u32, 59 | minimum_area: u32, 60 | pix: image::Rgb, 61 | ) { 62 | let mut rng = rand::thread_rng(); 63 | 64 | let bbox = BoundingBox::from_dimensions(img.width(), img.height()); 65 | let pieces = random_bbox_subdivisions(iterations, bbox, minimum_area, &mut rng); 66 | 67 | let mut drawer = Drawer::new_with_no_blending(img); 68 | 69 | for piece in pieces { 70 | // we cannot stipple lines therefore just draw a line. 71 | if piece.min().x >= piece.max().x || piece.min().y >= piece.max().y { 72 | drawer.line(*piece.min(), *piece.max(), &pix); 73 | continue; 74 | } 75 | 76 | stipple(&mut drawer, &piece, points, pix); 77 | } 78 | } 79 | 80 | /// Stipple the given bbox of the image with the desired amount of points. 81 | pub fn stipple( 82 | drawer: &mut Drawer, 83 | bbox: &BoundingBox, 84 | points: u32, 85 | pix: image::Rgb, 86 | ) { 87 | let mut rng = rand::thread_rng(); 88 | 89 | for _ in 0..points { 90 | let point = random_point_in_bbox(&mut rng, bbox); 91 | 92 | drawer.draw_pixel(point.x, point.y, &pix); 93 | } 94 | } 95 | 96 | fn initial_band(dir: Direction, width: u32, height: u32, bands: u32) -> BoundingBox { 97 | let band_width = width / bands; 98 | let band_height = height / bands; 99 | 100 | match dir { 101 | Direction::LeftToRight => BoundingBox::from_dimensions(band_width, height), 102 | Direction::RightToLeft => BoundingBox::from_dimensions_and_origin( 103 | &PointU32::new((bands - 1) * band_width, 0), 104 | band_width, 105 | height, 106 | ), 107 | Direction::TopToBottom => BoundingBox::from_dimensions(width, band_height), 108 | Direction::BottomToTop => BoundingBox::from_dimensions_and_origin( 109 | &PointU32::new(0, (bands - 1) * band_height), 110 | width, 111 | band_height, 112 | ), 113 | } 114 | } 115 | 116 | fn advance_band(band: &BoundingBox, dir: Direction) -> BoundingBox { 117 | let (band_width, band_height) = band.dimensions().unwrap(); 118 | 119 | let band_new_origin = match dir { 120 | Direction::LeftToRight => PointU32::new(band.max().x, 0), 121 | Direction::RightToLeft => PointU32::new(band.min().x - band_width, 0), 122 | Direction::TopToBottom => PointU32::new(0, band.max().y), 123 | Direction::BottomToTop => PointU32::new(0, band.min().y - band_height), 124 | }; 125 | 126 | BoundingBox::from_dimensions_and_origin(&band_new_origin, band_width, band_height) 127 | } 128 | -------------------------------------------------------------------------------- /mattors/art/voronoi.rs: -------------------------------------------------------------------------------- 1 | //! Draw some [Voronoi diagrams](https://en.wikipedia.org/wiki/Voronoi_diagram). 2 | 3 | use rand::Rng; 4 | 5 | use geo::{kdtree, BoundingBox, PointU32}; 6 | 7 | use crate::art::generate_distinct_random_points; 8 | use crate::color::{random_color, RandomColorConfig}; 9 | 10 | /// Generate a voronoi diagram where the colors are taken from the gradient 11 | /// going from color1 to color2. 12 | pub fn gradient_voronoi( 13 | img: &mut image::RgbImage, 14 | color1: image::Rgb, 15 | color2: image::Rgb, 16 | npoints: usize, 17 | ) { 18 | if npoints == 0 { 19 | return; 20 | } 21 | 22 | let random_points = generate_distinct_random_points( 23 | &mut rand::thread_rng(), 24 | npoints, 25 | &BoundingBox::from_dimensions(img.width(), img.height()), 26 | ); 27 | 28 | let points = random_points.iter().map(|pt| (*pt, ())).collect(); 29 | let points = kdtree::KdTree::from_vector(points); 30 | 31 | let dr = f64::from(color2[0]) - f64::from(color1[0]); 32 | let dg = f64::from(color2[1]) - f64::from(color1[1]); 33 | let db = f64::from(color2[2]) - f64::from(color1[2]); 34 | 35 | let img_width = img.width(); 36 | 37 | for (x, y, pix) in img.enumerate_pixels_mut() { 38 | let (closest_point, _) = points.nearest_neighbor(PointU32::new(x, y)).unwrap(); 39 | 40 | let c = f64::from(closest_point.x) / f64::from(img_width); 41 | *pix = image::Rgb([ 42 | (f64::from(color1[0]) + c * dr) as u8, 43 | (f64::from(color1[1]) + c * dg) as u8, 44 | (f64::from(color1[2]) + c * db) as u8, 45 | ]); 46 | } 47 | } 48 | 49 | /// Generate some random Voronoi diagrams. 50 | pub fn random_voronoi( 51 | img: &mut image::RgbImage, 52 | color_config: &mut RandomColorConfig, 53 | npoints: usize, 54 | ) { 55 | if npoints == 0 { 56 | return; 57 | } 58 | 59 | let random_points = generate_distinct_random_points( 60 | &mut rand::thread_rng(), 61 | npoints, 62 | &BoundingBox::from_dimensions(img.width(), img.height()), 63 | ); 64 | 65 | let points = random_points 66 | .iter() 67 | .map(|pt| (*pt, image::Rgb(random_color(color_config).to_rgb()))) 68 | .collect(); 69 | 70 | let points = kdtree::KdTree::from_vector(points); 71 | 72 | for (x, y, pix) in img.enumerate_pixels_mut() { 73 | let (_, closest_point_color) = points.nearest_neighbor(PointU32::new(x, y)).unwrap(); 74 | 75 | *pix = *closest_point_color; 76 | } 77 | 78 | // for point in random_points { 79 | // img.put_pixel(point.x, point.y, image::Rgb { data: [0, 0, 0] }); 80 | // } 81 | } 82 | -------------------------------------------------------------------------------- /mattors/drawing/line.rs: -------------------------------------------------------------------------------- 1 | //! Low level implementation details of line drawing algorithms. 2 | 3 | use std::mem; 4 | 5 | use geo::{Point, PointU32}; 6 | 7 | /// Iterator that returns all the points that compose the line from start to 8 | /// end. It uses the [Bresenham's line 9 | /// algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm) to 10 | /// interpolate the points in the line. Note that the points are returned in 11 | /// order that is if start is higher than end(i.e. start.y < end.y) then the 12 | /// points will be returned by starting from the top falling down. 13 | #[derive(Debug)] 14 | pub struct BresenhamLineIter { 15 | // this struct is designed to work for non steep lines. In case we actually 16 | // want to iterate over a steep line then the `new` function swaps x with y, 17 | // sets `is_steep` that is then checked in `next` and swaps x with y again 18 | // if the flag is set. It also assumes that `start` is the more "bottom 19 | // left" than `end`(this invariant is also ensured by `new`). 20 | start: Point, 21 | end: PointU32, 22 | is_steep: bool, 23 | d: i64, 24 | dx: i64, 25 | dy: i64, 26 | xstep: i64, 27 | ystep: i64, 28 | } 29 | 30 | impl BresenhamLineIter { 31 | /// Creates a new `BresenhamLineIter` iterator to return all points between 32 | /// `start` and `end` both included. 33 | pub fn new(mut start: PointU32, mut end: PointU32) -> BresenhamLineIter { 34 | let mut dx = (i64::from(end.x) - i64::from(start.x)).abs(); 35 | let mut dy = (i64::from(end.y) - i64::from(start.y)).abs(); 36 | 37 | let is_steep; 38 | 39 | // find out whether the line is steep that is that whether it grows faster 40 | // in y or in x and call the appropriate implementation. The algorithms are 41 | // the mirrors of each other, but the main idea is the same: the bump of the 42 | // slowest coordinate is governed by whether the value is closer to the new 43 | // coord or not. 44 | if dx >= dy { 45 | is_steep = false; 46 | } else { 47 | is_steep = true; 48 | 49 | mem::swap(&mut start.x, &mut start.y); 50 | mem::swap(&mut end.x, &mut end.y); 51 | mem::swap(&mut dx, &mut dy); 52 | } 53 | 54 | let xstep = if start.x > end.x { -1 } else { 1 }; 55 | let ystep = if start.y > end.y { -1 } else { 1 }; 56 | 57 | let start = Point { 58 | x: i64::from(start.x), 59 | y: i64::from(start.y), 60 | }; 61 | 62 | BresenhamLineIter { 63 | start, 64 | end, 65 | is_steep, 66 | dx, 67 | dy, 68 | d: 2 * dy - dx, 69 | ystep, 70 | xstep, 71 | } 72 | } 73 | 74 | // calculate next non steep point in the line 75 | fn next_non_steep_point(&mut self) -> Option { 76 | if (self.start.x > i64::from(self.end.x) && self.xstep > 0) 77 | || (self.start.x < i64::from(self.end.x) && self.xstep < 0) 78 | { 79 | return None; 80 | } 81 | 82 | if self.start.x < 0 || self.start.y < 0 { 83 | return None; 84 | } 85 | 86 | let old = PointU32 { 87 | x: self.start.x as u32, 88 | y: self.start.y as u32, 89 | }; 90 | 91 | if self.d > 0 { 92 | self.start.y += self.ystep; 93 | self.d -= 2 * self.dx; 94 | } 95 | 96 | self.d += 2 * self.dy; 97 | 98 | self.start.x += self.xstep; 99 | 100 | Some(old) 101 | } 102 | } 103 | 104 | impl Iterator for BresenhamLineIter { 105 | type Item = PointU32; 106 | 107 | fn next(&mut self) -> Option { 108 | self.next_non_steep_point().map(|mut res| { 109 | // in case the line is steep then we need to swap back the 110 | // coordinates before returning to reverse the swap done in `new`. 111 | if self.is_steep { 112 | mem::swap(&mut res.x, &mut res.y); 113 | } 114 | res 115 | }) 116 | } 117 | } 118 | 119 | #[cfg(test)] 120 | mod tests { 121 | use super::*; 122 | 123 | fn _test_line_bresenham(start: PointU32, end: PointU32, exp_points: Vec) { 124 | assert_eq!( 125 | BresenhamLineIter::new(start, end).collect::>(), 126 | exp_points, 127 | "line from start {:?} to end {:?}", 128 | start, 129 | end, 130 | ); 131 | 132 | assert_eq!( 133 | BresenhamLineIter::new(end, start).collect::>(), 134 | exp_points.iter().cloned().rev().collect::>(), 135 | "line from end {:?} to start {:?}", 136 | end, 137 | start, 138 | ); 139 | } 140 | 141 | #[test] 142 | fn test_bresenham_line_basic() { 143 | let origin = Point { x: 0, y: 0 }; 144 | 145 | _test_line_bresenham(origin, origin, vec![origin]); 146 | 147 | let bis = Point { x: 3, y: 3 }; 148 | let bis_exp_points = vec![origin, Point { x: 1, y: 1 }, Point { x: 2, y: 2 }, bis]; 149 | 150 | _test_line_bresenham(origin, bis, bis_exp_points); 151 | } 152 | 153 | #[test] 154 | fn test_bresenham_line_non_steep() { 155 | let origin = Point { x: 0, y: 0 }; 156 | let non_steep_pt = Point { x: 3, y: 1 }; 157 | let exp_points = vec![ 158 | origin, 159 | Point { x: 1, y: 0 }, 160 | Point { x: 2, y: 1 }, 161 | non_steep_pt, 162 | ]; 163 | 164 | _test_line_bresenham(origin, non_steep_pt, exp_points); 165 | } 166 | 167 | #[test] 168 | fn test_bresenham_line_steep() { 169 | let origin = Point { x: 0, y: 0 }; 170 | let steep_pt = Point { x: 1, y: 3 }; 171 | let exp_points = vec![origin, Point { x: 0, y: 1 }, Point { x: 1, y: 2 }, steep_pt]; 172 | 173 | _test_line_bresenham(origin, steep_pt, exp_points); 174 | } 175 | 176 | #[test] 177 | fn test_bresenham_line_dec() { 178 | let start = Point { x: 4, y: 0 }; 179 | let end = Point { x: 1, y: 3 }; 180 | let exp_points = vec![start, Point { x: 3, y: 1 }, Point { x: 2, y: 2 }, end]; 181 | 182 | _test_line_bresenham(start, end, exp_points); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /mattors/drawing/triangle.rs: -------------------------------------------------------------------------------- 1 | //! Low level implementation details of the triangle primitive. 2 | 3 | use geo::PointU32; 4 | 5 | use crate::drawing::line::BresenhamLineIter; 6 | 7 | /// Iterator that returns the edge points of a flat triangle that is a triangle 8 | /// that has at least 2 points on the same y. 9 | pub struct FlatTriangleIter { 10 | last_start: PointU32, 11 | last_end: PointU32, 12 | p1p2_iter: BresenhamLineIter, 13 | p1p3_iter: BresenhamLineIter, 14 | over: bool, 15 | } 16 | 17 | impl FlatTriangleIter { 18 | /// Create a new `FlatTriangleIter`. 19 | /// invariant: `p2` and `p3` are the points on the flat line. 20 | pub fn new(p1: PointU32, p2: PointU32, p3: PointU32) -> FlatTriangleIter { 21 | FlatTriangleIter { 22 | last_start: p1, 23 | last_end: p1, 24 | p1p2_iter: BresenhamLineIter::new(p1, p2), 25 | p1p3_iter: BresenhamLineIter::new(p1, p3), 26 | over: false, 27 | } 28 | } 29 | } 30 | 31 | impl Iterator for FlatTriangleIter { 32 | type Item = (PointU32, PointU32); 33 | 34 | fn next(&mut self) -> Option { 35 | if self.over { 36 | return None; 37 | } 38 | 39 | let res = (self.last_start, self.last_end); 40 | 41 | // advance the current points, but make sure the y coord actually 42 | // changes because otherwise we could potentially draw a line on the 43 | // same y coordinates multiple times. 44 | loop { 45 | match self.p1p2_iter.next() { 46 | Some(new_start) => { 47 | if new_start.y != self.last_start.y { 48 | self.last_start = new_start; 49 | break; 50 | } 51 | } 52 | None => { 53 | self.over = true; 54 | break; 55 | } 56 | } 57 | } 58 | 59 | loop { 60 | match self.p1p3_iter.next() { 61 | Some(new_end) => { 62 | if new_end.y != self.last_end.y { 63 | self.last_end = new_end; 64 | break; 65 | } 66 | } 67 | None => { 68 | self.over = true; 69 | break; 70 | } 71 | } 72 | } 73 | 74 | Some(res) 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::*; 81 | use geo::Point; 82 | 83 | #[test] 84 | fn test_flat_upper_triangle_iter() { 85 | let p1 = Point::new(4, 0); 86 | let p2 = Point::new(2, 2); 87 | let p3 = Point::new(8, 2); 88 | 89 | let exp_points = vec![ 90 | (p1, p1), 91 | (PointU32::new(3, 1), PointU32::new(6, 1)), 92 | (p2, p3), 93 | ]; 94 | 95 | assert_eq!( 96 | FlatTriangleIter::new(p1, p2, p3).collect::>(), 97 | exp_points 98 | ); 99 | } 100 | 101 | #[test] 102 | fn test_flat_bottom_triangle_iter() { 103 | let p1 = Point::new(2, 0); 104 | let p2 = Point::new(6, 0); 105 | let p3 = Point::new(4, 2); 106 | 107 | let exp_points = vec![ 108 | (p3, p3), 109 | (PointU32::new(3, 1), PointU32::new(5, 1)), 110 | (p1, p2), 111 | ]; 112 | 113 | assert_eq!( 114 | FlatTriangleIter::new(p3, p1, p2).collect::>(), 115 | exp_points 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /mattors/lib.rs: -------------------------------------------------------------------------------- 1 | //! Simple module to create some generative art. 2 | #![deny(missing_docs, warnings)] 3 | 4 | pub mod art; 5 | pub mod color; 6 | pub mod drawing; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mattors", 3 | "version": "1.0.0", 4 | "description": "Generative art playground", 5 | "main": "index.js", 6 | "repository": "git@github.com:danieledapo/mattors.git", 7 | "author": "Daniele D'Orazio ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@types/p5": "^0.9", 11 | "@types/react": "^17.0.0", 12 | "@types/react-dom": "^17.0.0", 13 | "@types/react-router-dom": "^5.1.3", 14 | "autoprefixer": "^10.1.0", 15 | "awesome-typescript-loader": "^5.2.1", 16 | "css-loader": "^5.0.1", 17 | "html-webpack-plugin": "^4.5.0", 18 | "mini-css-extract-plugin": "^1.3.3", 19 | "node-sass": "^5.0.0", 20 | "postcss-loader": "^4.1.0", 21 | "sass": "^1.24.0", 22 | "sass-loader": "^10.1.0", 23 | "source-map-loader": "^2.0.0", 24 | "style-loader": "^2.0.0", 25 | "tslint": "^6.1.3", 26 | "typescript": "^4.1.3", 27 | "webpack": "^5.11.1", 28 | "webpack-cli": "^4.3.1", 29 | "webpack-dev-server": "^3.10.1" 30 | }, 31 | "dependencies": { 32 | "react": "^17.0.1", 33 | "react-dom": "^17.0.1", 34 | "react-router-dom": "^5.1.2", 35 | "spectre.css": "^0.5.8" 36 | }, 37 | "scripts": { 38 | "build": "webpack --mode production", 39 | "lint": "tslint --project .", 40 | "start": "webpack serve --mode development --open" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // this file is needed only to compile specre.css correctly 2 | 3 | module.exports = { 4 | plugins: [ 5 | require('autoprefixer') 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /scripts/cargo-debug-macro: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cargo +nightly rustc --profile test --lib -- -Z external-macro-backtrace -Z unstable-options --pretty=expanded 4 | -------------------------------------------------------------------------------- /scripts/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | yarn build 6 | 7 | git add . 8 | git commit 9 | 10 | git push origin HEAD 11 | git subtree push --prefix dist origin gh-pages 12 | -------------------------------------------------------------------------------- /sketches/annulus.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class Annulus implements ISketch { 4 | public readonly name = "Annulus"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = false; 9 | 10 | // expose to the ui 11 | public readonly nArcs = 1000; 12 | 13 | public reset(p: p5) { 14 | p.background("black"); 15 | } 16 | 17 | public draw(p: p5) { 18 | 19 | const center: [number, number] = [this.width / 2, this.height / 2]; 20 | 21 | const palette = [ 22 | // "#a9e5bb", 23 | "#fcf6b1", 24 | "#f7b32b", 25 | "#f72c25", 26 | "#2d1e2f", 27 | ]; 28 | 29 | p.noFill(); 30 | 31 | let radius = 0; 32 | 33 | for (let i = 0; i < this.nArcs; ++i) { 34 | const color = palette[Math.floor(p.random(0, palette.length))]; 35 | p.stroke(color); 36 | 37 | const newRadius = radius + p.random(1, 8); 38 | 39 | p.strokeWeight(newRadius - radius); 40 | 41 | const startAngle = p.random(0, p.TWO_PI); 42 | const endAngle = p.random(0, p.TWO_PI); 43 | 44 | p.arc(center[0], center[1], newRadius, newRadius, startAngle, endAngle); 45 | 46 | radius = newRadius; 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /sketches/astroid.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | import { randomPointInCircle, sampleCircle } from "./utils"; 3 | 4 | // astroid, a.k.a. Planet of the apes Caesar window 5 | export class Astroid implements ISketch { 6 | public readonly name = "Astroid"; 7 | 8 | public readonly width = 600; 9 | public readonly height = 600; 10 | public readonly loop = false; 11 | 12 | public readonly radius = 200; 13 | public readonly step = Math.PI / 24; 14 | public readonly perturbations = 30; 15 | public readonly maxRoughness = 20; 16 | 17 | // tslint:disable-next-line:no-empty 18 | public reset() { } 19 | 20 | public draw(p: p5) { 21 | this.blackboardBackground(p); 22 | this.astroid(p); 23 | this.snow(p); 24 | } 25 | 26 | private blackboardBackground(p: p5, linesCount: number = 5000) { 27 | p.background("#4e5a65"); 28 | 29 | p.stroke(255, 255, 255, 5); 30 | p.strokeWeight(3); 31 | 32 | for (let i = 0; i < linesCount; ++i) { 33 | p.line( 34 | p.random(0, this.width), 35 | p.random(0, this.height), 36 | p.random(0, this.width), 37 | p.random(0, this.height), 38 | ); 39 | } 40 | } 41 | 42 | private astroid(p: p5) { 43 | p.push(); 44 | 45 | p.translate(this.width / 2, this.height / 2); 46 | 47 | p.stroke(255, 255, 255, 30); 48 | p.strokeWeight(3); 49 | 50 | let prev: [number, number] | null = null; 51 | for (const c of sampleCircle(this.step, this.radius)) { 52 | if (prev !== null) { 53 | drawApproximatedLine(p, prev, c, this.perturbations, this.maxRoughness); 54 | } 55 | 56 | prev = c; 57 | } 58 | 59 | prev = null; 60 | for (const astro of astroid(this.step, this.radius)) { 61 | if (prev !== null) { 62 | drawApproximatedLine(p, prev, astro, this.perturbations, this.maxRoughness); 63 | } 64 | 65 | prev = astro; 66 | } 67 | 68 | p.pop(); 69 | } 70 | 71 | private snow(p: p5, snowflakesCount: number = 1000) { 72 | p.stroke(255, 255, 255, 5); 73 | p.strokeWeight(3); 74 | 75 | for (let i = 0; i < snowflakesCount; ++i) { 76 | p.push(); 77 | 78 | const cx = p.random(0, this.width); 79 | const cy = p.random(0, this.height); 80 | const cr = p.random(1, 10); 81 | 82 | p.translate(cx, cy); 83 | 84 | for (let j = 0; j < 5; ++j) { 85 | const p1 = randomPointInCircle(p, cr); 86 | const p2 = randomPointInCircle(p, cr); 87 | 88 | drawApproximatedLine(p, p1, p2, 5, 3); 89 | } 90 | 91 | p.pop(); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Approximate an astroid https://en.wikipedia.org/wiki/Astroid returning the 98 | * points. 99 | * @param tstep step angle in radians to sample the astroid at 100 | * @param radius radius of the astroid 101 | */ 102 | export function* astroid(tstep: number, radius: number): Iterable<[number, number]> { 103 | for (let t = 0; t <= Math.PI * 2; t += tstep) { 104 | const x = radius / 4 * (3 * Math.cos(t) + Math.cos(3 * t)); 105 | const y = radius / 4 * (3 * Math.sin(t) - Math.sin(3 * t)); 106 | 107 | yield [x, y]; 108 | } 109 | } 110 | 111 | /** 112 | * Draw a perturbed line by drawing `permutations` lines where each coordinate 113 | * is slightly randomly offset. 114 | */ 115 | export function drawApproximatedLine( 116 | p: p5, 117 | p0: [number, number], 118 | p1: [number, number], 119 | perturbations: number, 120 | maxRoughness: number, 121 | ) { 122 | for (let it = 0; it < perturbations; ++it) { 123 | const roughness = p.random(0, maxRoughness); 124 | p.line( 125 | p0[0] + p.random(-roughness, roughness), 126 | p0[1] + p.random(-roughness, roughness), 127 | p1[0] + p.random(-roughness, roughness), 128 | p1[1] + p.random(-roughness, roughness), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /sketches/blankets.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class Blankets implements ISketch { 4 | public readonly name = "Blankets"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | 10 | public readonly gridSize = 10; 11 | 12 | private t = 0; 13 | 14 | public reset(p: p5) { 15 | this.t = p.random(); 16 | p.frameRate(2); 17 | 18 | p.background("black"); 19 | } 20 | 21 | public draw(p: p5) { 22 | p.stroke(200, 200, 200, 10); 23 | 24 | this.drawSheet(p, 0.05); 25 | } 26 | 27 | public drawSheet(p: p5, div: number) { 28 | const startx = p.random(this.width); 29 | const starty = p.random(this.height); 30 | 31 | let t = this.t; 32 | for (let ti = 0; ti < 5; ti += 0.01) { 33 | let ptx = startx; 34 | let pty = starty; 35 | 36 | for (let i = 0; i < 500; i++) { 37 | const a = p.noise(ptx / this.gridSize * div, pty / this.gridSize * div, t) * p.TWO_PI; 38 | 39 | const nptx = ptx + Math.cos(a) * this.gridSize; 40 | const npty = pty + Math.sin(a) * this.gridSize; 41 | 42 | p.line(ptx, pty, nptx, npty); 43 | 44 | ptx = nptx; 45 | pty = npty; 46 | 47 | if (ptx < 0 || ptx > this.width || pty < 0 || pty > this.height) { 48 | break; 49 | } 50 | } 51 | 52 | t += 0.001; 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sketches/bloody-spider-web.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class BloodySpiderWeb implements ISketch { 4 | public readonly name = "Bloody spider web"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = false; 9 | 10 | public readonly lines = 1000; 11 | 12 | public reset(p: p5) { 13 | p.background("black"); 14 | } 15 | 16 | public draw(p: p5) { 17 | const moonRadius = this.width / 2; 18 | 19 | const moonCenterX = this.width / 2 + p.random(-moonRadius, moonRadius) / 2; 20 | const moonCenterY = this.height / 2 + p.random(-moonRadius, moonRadius) / 2; 21 | 22 | for (let i = 0; i < this.lines; ++i) { 23 | const [x1, y1] = this.randomPointInCircle(p, [moonCenterX, moonCenterY], moonRadius); 24 | const [x2, y2] = this.randomPointInCircle(p, [moonCenterX, moonCenterY], moonRadius); 25 | 26 | if (p.random() >= 0.8) { 27 | p.strokeWeight(p.random(1, 10)); 28 | p.stroke(187, 10, 30, 10); 29 | } else { 30 | p.strokeWeight(p.random(1, 10)); 31 | p.stroke(255, 255, 255, 10); 32 | } 33 | 34 | p.line(x1, y1, x2, y2); 35 | } 36 | } 37 | 38 | private randomPointInCircle(p: p5, [cx, cy]: [number, number], radius: number): [number, number] { 39 | const angle = p.random(0, p.TWO_PI); 40 | const r = p.random(0, radius); 41 | 42 | return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)]; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /sketches/bw-rain.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class BlackWhiteRain implements ISketch { 4 | public readonly name = "Black and White Rain"; 5 | 6 | public readonly width = 841; 7 | public readonly height = 1189; 8 | public readonly loop = true; 9 | 10 | public reset(p: p5) { 11 | p.background("white"); 12 | } 13 | 14 | public draw(p: p5) { 15 | p.background("white"); 16 | 17 | const nDiags = p.random(20, 200); 18 | const horStep = p.random(2, 5); 19 | 20 | for (let x = 0; x < this.width; x += horStep) { 21 | let s = horStep + ((x / nDiags) * this.width) / this.height; 22 | 23 | for (let y = 0; y < this.height; y += s) { 24 | const t = Math.max((y + s) / this.height, (x + horStep) / this.width); 25 | 26 | if (p.random() > t) { 27 | p.line(x, y, x, y + s); 28 | } 29 | 30 | s *= 1.00005; 31 | } 32 | } 33 | p.frameRate(1); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sketches/cairo-tiling.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ISketch, 3 | } from "./sketch"; 4 | 5 | interface Pentagon { 6 | cx: number, 7 | cy: number, 8 | size: number, 9 | } 10 | 11 | const sqrt3 = Math.sqrt(3); 12 | const smallSideLen = sqrt3 - 1; 13 | 14 | // https://en.wikipedia.org/wiki/Cairo_pentagonal_tiling 15 | export class CairoTiling implements ISketch { 16 | public readonly name = "Cairo tiling"; 17 | 18 | public readonly width = 1920; 19 | public readonly height = 1080; 20 | public readonly loop = true; 21 | 22 | private stack: Pentagon[] = []; 23 | private alreadySeen: Pentagon[] = []; 24 | 25 | private palettePoints: [number, number][] = []; 26 | private palette = [ 27 | "#fdef96", 28 | "#f7b71d", 29 | "#afa939", 30 | "#2b580c", 31 | ]; 32 | 33 | public reset(p: p5) { 34 | p.background("white"); 35 | 36 | this.stack = [{ 37 | cx: this.width / 2, 38 | cy: this.height / 2, 39 | size: 20, 40 | }]; 41 | 42 | this.alreadySeen.splice(0, this.alreadySeen.length); 43 | this.palettePoints.splice(0, this.palettePoints.length); 44 | 45 | const nPalettePoints = 3; 46 | for (let i = 0; i < nPalettePoints; ++i) { 47 | this.palettePoints.push([ 48 | i / nPalettePoints * this.width + p.random(this.width / (nPalettePoints - 1)), 49 | p.random(this.height) 50 | ]); 51 | } 52 | 53 | // this.palettePoints = [[this.width / 2, this.height / 2]]; 54 | } 55 | 56 | public draw(p: p5) { 57 | while (this.stack.length > 0) { 58 | const c = this.stack.pop()!; 59 | const { cx, cy, size } = c; 60 | 61 | if (this.isPentagonHidden(c)) { 62 | continue; 63 | } 64 | 65 | if (this.hasSeen(cx, cy)) { 66 | continue; 67 | } 68 | this.alreadySeen.push(c); 69 | 70 | this.drawTile(p, c); 71 | 72 | const sl = size * smallSideLen; 73 | const sh = (sl / 2 * sqrt3); 74 | this.stack.push( 75 | { 76 | cx: cx + (2 * size) + (sl / 2), 77 | cy: cy - sh, 78 | size, 79 | }, 80 | { 81 | cx: cx - (2 * size) - (sl / 2), 82 | cy: cy + sh, 83 | size, 84 | }, 85 | { 86 | cx: cx + sh, 87 | cy: cy + 2 * size + sl / 2, 88 | size, 89 | }, 90 | { 91 | cx: cx - sh, 92 | cy: cy - 2 * size - sl / 2, 93 | size, 94 | }, 95 | 96 | ); 97 | 98 | break; 99 | } 100 | } 101 | 102 | private drawTile(p: p5, pentagon: Pentagon) { 103 | let closestX = 0; 104 | let closestY = 0; 105 | let mind = Infinity; 106 | for (const [x, y] of this.palettePoints) { 107 | let d = Math.abs(x - pentagon.cx) + Math.abs(y - pentagon.cy); 108 | if (d < mind) { 109 | mind = d; 110 | closestX = x; 111 | closestY = y; 112 | } 113 | } 114 | 115 | let maxD = Math.abs(closestX + closestY); 116 | let t = mind / maxD; 117 | let f = this.palette[Math.min(Math.floor(t * this.palette.length), this.palette.length - 1)]; 118 | 119 | p.push(); 120 | p.fill(f); 121 | p.strokeWeight(3); 122 | p.translate(pentagon.cx, pentagon.cy); 123 | for (let i = 0; i < 4; ++i) { 124 | p.rotate(p.HALF_PI); 125 | this.drawPentagon(p, pentagon.size); 126 | } 127 | p.pop(); 128 | } 129 | 130 | private drawPentagon(p: p5, size: number) { 131 | const points = [ 132 | [0, 0], 133 | [0, -1], 134 | [-smallSideLen / 2 * sqrt3, -1 - smallSideLen / 2], 135 | [-1.5, -sqrt3 / 2], 136 | [-1, 0] 137 | ]; 138 | 139 | p.beginShape(); 140 | for (const [x, y] of points) { 141 | p.vertex(x * size, y * size); 142 | } 143 | p.endShape(p.CLOSE); 144 | } 145 | 146 | private hasSeen(cx: number, cy: number): boolean { 147 | const ix = this.alreadySeen.findIndex((p) => { 148 | return Math.abs(p.cx - cx) < 1e-6 && Math.abs(p.cy - cy) < 1e-6; 149 | }); 150 | return ix >= 0; 151 | } 152 | 153 | private isPentagonHidden(c: Pentagon): boolean { 154 | const { cx, cy, size } = c; 155 | 156 | // i'm too lazy to do the correct math to know the proper max delta, 157 | // 1.5 is enough... 158 | const delta = size * 1.5; 159 | return (cx + delta < 0 || cy + delta < 0 || cx - delta > this.width || cy - delta > this.height); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /sketches/chaikin.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class Chaikin implements ISketch { 4 | public readonly name = "Chaikin"; 5 | 6 | public readonly width = 800; 7 | public readonly height = 800; 8 | public readonly loop = false; 9 | 10 | public reset(p: p5) { 11 | p.background("white"); 12 | // p.frameRate(10); 13 | } 14 | 15 | public draw(p: p5) { 16 | p.push(); 17 | 18 | const steps = 4; 19 | const points = 100; 20 | 21 | let path = []; 22 | for (let i = 0; i < points; ++i) { 23 | path.push( 24 | p.createVector((i / (points - 1)) * this.width, (p.random() * this.height) / steps), 25 | ); 26 | } 27 | 28 | p.noFill(); 29 | for (let i = 1; i < steps; ++i) { 30 | p.beginShape(); 31 | for (const pt of path) { 32 | p.vertex(pt.x, pt.y); 33 | } 34 | p.endShape(); 35 | 36 | const newPath: p5.Vector[] = []; 37 | 38 | for (let j = 0; j < path.length - 1; ++j) { 39 | newPath.push( 40 | path[j] 41 | .copy() 42 | .mult(0.75) 43 | .add(path[j + 1].copy().mult(0.25)), 44 | path[j] 45 | .copy() 46 | .mult(0.25) 47 | .add(path[j + 1].copy().mult(0.75)), 48 | ); 49 | } 50 | path = newPath; 51 | 52 | p.translate(0, this.height / steps); 53 | } 54 | 55 | p.pop(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sketches/circular-maze.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class CircularMaze implements ISketch { 4 | public readonly name = "Circular Maze (kind of)"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = false; 9 | 10 | public reset(p: p5) { 11 | const h1 = p.random(360); 12 | const h2 = (h1 + 180) % 360; 13 | 14 | p.colorMode(p.HSB); 15 | p.background(h1, 80, 80); 16 | p.stroke(h2, 80, 80); 17 | p.noFill(); 18 | p.rect(0, 0, this.width, this.height); 19 | } 20 | 21 | public draw(p: p5) { 22 | p.push(); 23 | p.translate(this.width / 2, this.height / 2); 24 | 25 | const r = Math.min(this.width, this.height) / 2 - 20; 26 | const nested = Math.floor(p.random(3, 20)); 27 | const divs = Math.floor(p.random(10, 20)); 28 | 29 | const pl = p.random(0.7); 30 | const pa = p.random(0.7); 31 | 32 | p.noFill(); 33 | p.strokeWeight(5); 34 | 35 | for (let i = 0; i < nested - 1; ++i) { 36 | const t = i / (nested - 1); 37 | 38 | for (let j = 0; j < divs; ++j) { 39 | if (p.random() > pa) { 40 | p.arc( 41 | 0, 42 | 0, 43 | r * 2 * t, 44 | r * 2 * t, 45 | (p.TWO_PI * j) / divs, 46 | (p.TWO_PI * (j + 1)) / divs, 47 | ); 48 | } 49 | } 50 | } 51 | p.ellipse(0, 0, r * 2, r * 2); 52 | 53 | for (let i = 0; i < divs; ++i) { 54 | const a = p.TWO_PI * (i / divs); 55 | 56 | for (let j = 0; j < nested - 1; ++j) { 57 | const sr = r * (j / (nested - 1)); 58 | const br = r * ((j + 1) / (nested - 1)); 59 | 60 | if (p.random() > pl) { 61 | p.line(sr * Math.cos(a), sr * Math.sin(a), br * Math.cos(a), br * Math.sin(a)); 62 | } 63 | } 64 | } 65 | 66 | p.pop(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sketches/clifford-attractors.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class CliffordAttractors implements ISketch { 4 | public readonly name = "Clifford Attractors"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | 10 | private a = -1.4; 11 | private b = 1.6; 12 | private c = 1; 13 | private d = 0.7; 14 | 15 | private x = 0; 16 | private y = 0; 17 | private alreadySeen = new Set(); 18 | 19 | public reset(p: p5) { 20 | p.background("black"); 21 | 22 | // this.a = -2; 23 | // this.b = 1.6; 24 | // this.c = -2; 25 | // this.d = 0.7; 26 | 27 | this.alreadySeen.clear(); 28 | 29 | this.a = p.random([1, -1]) * p.random(1, 2); 30 | this.b = p.random([1, -1]) * p.random(1, 2); 31 | this.c = p.random([1, -1]) * p.random(1, 2); 32 | this.d = p.random([1, -1]) * p.random(1, 2); 33 | 34 | console.log(">>> clifford attractors: parameters", this.a, this.b, this.c, this.d); 35 | } 36 | 37 | public draw(p: p5) { 38 | p.translate(this.width / 2, this.height / 2); 39 | 40 | for (let i = 0; i < 500; ++i) { 41 | const nx = (Math.sin(this.a * this.y) + this.c * Math.cos(this.a * this.x)); 42 | const ny = (Math.sin(this.b * this.x) + this.d * Math.cos(this.b * this.y)); 43 | 44 | p.stroke(255, 255, 255, 50); 45 | p.fill(255, 255, 255, 50); 46 | 47 | p.point(nx * 200, ny * 200); 48 | 49 | // some attractors form very small loops which are not particularly 50 | // interesting to render, just skip them 51 | if (this.alreadySeen.has([nx, ny].toString()) && this.alreadySeen.size < 500) { 52 | console.log(">>> clifford attractors: skipping because it didn't have enough variance"); 53 | this.reset(p); 54 | return; 55 | } 56 | 57 | this.alreadySeen.add([nx, ny].toString()); 58 | 59 | this.x = nx; 60 | this.y = ny; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sketches/cubic-disarray.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ISketch, 3 | } from "./sketch"; 4 | 5 | // Port of George Ness Cubic Disarray piece 6 | export class CubicDisarray implements ISketch { 7 | public readonly name = "Cubic disarray"; 8 | 9 | public readonly borderMargin = 30; 10 | public readonly width = 600; 11 | public readonly height = 720; 12 | public readonly loop = false; 13 | 14 | public readonly cols = 20; 15 | public readonly rows = 24; 16 | 17 | public readonly crazinessPerRow = 3; 18 | 19 | public reset(p: p5) { 20 | p.background("white"); 21 | } 22 | 23 | public draw(p: p5) { 24 | p.noFill(); 25 | p.rectMode(p.CENTER); 26 | 27 | p.push(); 28 | 29 | p.translate(this.borderMargin, this.borderMargin); 30 | 31 | const rectWidth = (this.width - this.borderMargin * 2) / this.cols; 32 | const rectHeight = (this.height - this.borderMargin * 2) / this.rows; 33 | 34 | let cols = this.cols; 35 | 36 | for (let y = 0; y < this.rows; ++y) { 37 | const rowWeight = y / (this.rows * this.crazinessPerRow); 38 | 39 | for (let x = 0; x < cols; ++x) { 40 | p.push(); 41 | 42 | p.translate( 43 | x * rectWidth + rectWidth / 2, 44 | y * rectHeight + rectHeight / 2, 45 | ); 46 | 47 | this.drawRect(p, rectWidth, rectHeight, rowWeight); 48 | 49 | p.pop(); 50 | } 51 | 52 | cols -= y / this.rows; 53 | } 54 | 55 | p.pop(); 56 | } 57 | 58 | private drawRect(p: p5, rectWidth: number, rectHeight: number, rowWeight: number) { 59 | p.translate( 60 | p.random(-rectWidth * rowWeight, rectWidth * rowWeight), 61 | p.random(-rectHeight * rowWeight, rectHeight * rowWeight), 62 | ); 63 | 64 | const rotation = p.random(-p.HALF_PI * rowWeight, p.HALF_PI * rowWeight); 65 | p.rotate(rotation); 66 | 67 | p.rect(0, 0, rectWidth - 1, rectHeight - 1); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sketches/cuts.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class Cuts implements ISketch { 4 | public readonly name = "Cuts and Tears"; 5 | 6 | public readonly width = 600; 7 | public readonly height = 600; 8 | public readonly loop = false; 9 | 10 | private readonly padding = 30; 11 | private readonly nlines = 30; 12 | 13 | public reset(p: p5) { 14 | p.background(80, 80, 80); 15 | } 16 | 17 | public draw(p: p5) { 18 | const ph = p.random(0, 0.3); 19 | const pv = 1 - ph; 20 | 21 | p.push(); 22 | for (let i = 0; i < this.nlines; ++i) { 23 | p.translate(0, this.height / (this.nlines + 1)); 24 | 25 | if (p.random() > ph) { 26 | this.drawCurve(p, i); 27 | } 28 | } 29 | p.pop(); 30 | 31 | for (let i = 0; i < this.nlines; ++i) { 32 | p.translate(this.width / (this.nlines + 1), 0); 33 | 34 | if (p.random() > pv) { 35 | p.push(); 36 | p.rotate(p.HALF_PI); 37 | this.drawCurve(p, i / this.nlines); 38 | p.pop(); 39 | } 40 | } 41 | } 42 | 43 | public drawCurve(p: p5, i: number) { 44 | const c = p.color(255, 255, 255, 10); 45 | 46 | const t = i / this.nlines; 47 | 48 | const maxAmpl = this.width / this.nlines / 2; 49 | const amp = maxAmpl * 0.25 + p.noise(t) * maxAmpl * 0.75; 50 | const aoff = p.noise(t, amp) * 2 * p.TWO_PI; 51 | 52 | const startOffset = p.random(this.width / 3); 53 | const endOffset = p.random(this.width / 3); 54 | 55 | for (let x = this.padding + startOffset; x < this.width - this.padding - endOffset; x += 1) { 56 | c.setAlpha(p.alpha(c) + 130 / this.width); 57 | p.stroke(c); 58 | p.strokeCap(p.SQUARE); 59 | p.noFill(); 60 | 61 | p.ellipse( 62 | x, 63 | -Math.cos(aoff + p.map(x, 0, this.width, 0, p.TWO_PI)) * amp, 64 | 1, 65 | 1, 66 | ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sketches/dla.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | interface Particle { 4 | position: p5.Vector; 5 | radius: number; 6 | parent: null | Particle; 7 | } 8 | 9 | class Bbox { 10 | constructor(public readonly lower: p5.Vector, public readonly upper: p5.Vector) {} 11 | 12 | public expand(p: p5.Vector) { 13 | this.lower.x = Math.min(this.lower.x, p.x); 14 | this.lower.y = Math.min(this.lower.y, p.y); 15 | 16 | this.upper.x = Math.max(this.upper.x, p.x); 17 | this.upper.y = Math.max(this.upper.y, p.y); 18 | } 19 | 20 | public contains(p: p5.Vector): boolean { 21 | return ( 22 | this.lower.x <= p.x && this.upper.x >= p.x && this.lower.y <= p.y && this.upper.y >= p.y 23 | ); 24 | } 25 | 26 | public copy(): Bbox { 27 | return new Bbox(this.lower.copy(), this.upper.copy()); 28 | } 29 | 30 | public enlarge(r: number) { 31 | this.lower.x -= r; 32 | this.lower.y -= r; 33 | 34 | this.upper.x += r; 35 | this.upper.y += r; 36 | } 37 | 38 | public center(): p5.Vector { 39 | return this.lower 40 | .copy() 41 | .add(this.upper) 42 | .div(2); 43 | } 44 | } 45 | 46 | export class Dla implements ISketch { 47 | public readonly name = "Diffusion limited aggregation"; 48 | 49 | public readonly width = 1920; 50 | public readonly height = 1080; 51 | public readonly loop = true; 52 | 53 | private particles: Particle[] = []; 54 | private particlesBbox: Bbox = new Bbox(new p5.Vector(), new p5.Vector()); 55 | private containerRadius = 0; 56 | private stickiness = 1; 57 | 58 | public reset(p: p5) { 59 | p.background("black"); 60 | 61 | p.noFill(); 62 | p.colorMode(p.HSB); 63 | p.stroke(p.random(360), 50, 50, 0.4); 64 | 65 | this.particles = [ 66 | { 67 | position: p.createVector(), 68 | radius: 16, 69 | parent: null, 70 | }, 71 | ]; 72 | 73 | this.particlesBbox = new Bbox( 74 | p.createVector(Infinity, Infinity), 75 | p.createVector(-Infinity, -Infinity), 76 | ); 77 | for (const pa of this.particles) { 78 | this.particlesBbox.expand(pa.position); 79 | } 80 | 81 | this.containerRadius = Math.min(this.width / 2, this.height / 2) - 50 * 2; 82 | this.stickiness = p.random(1); 83 | } 84 | 85 | public draw(p: p5) { 86 | p.push(); 87 | p.translate(this.width / 2, this.height / 2); 88 | 89 | const spawnBbox = this.particlesBbox.copy(); 90 | spawnBbox.enlarge(50); 91 | 92 | let particle: Particle | undefined = undefined; 93 | 94 | while (true) { 95 | if (particle === undefined) { 96 | particle = { 97 | position: p.createVector( 98 | p.random(spawnBbox.lower.x, spawnBbox.upper.x), 99 | p.random(spawnBbox.lower.y, spawnBbox.upper.y), 100 | ), 101 | radius: 8, 102 | parent: null, 103 | }; 104 | 105 | particle.position.limit(this.containerRadius); 106 | } 107 | 108 | if (!spawnBbox.contains(particle.position)) { 109 | particle = undefined; 110 | continue; 111 | } 112 | 113 | const neighbor = this.particles.find(e => { 114 | return e.position.dist(particle!.position) < e.radius + particle!.radius; 115 | }); 116 | 117 | if (neighbor === undefined || p.random() > this.stickiness) { 118 | particle.position.add(p5.Vector.random2D().mult(particle.radius)); 119 | continue; 120 | } 121 | 122 | particle.parent = neighbor; 123 | 124 | this.particles.push(particle); 125 | this.particlesBbox.expand(particle.position); 126 | 127 | p.ellipse(particle.position.x, particle.position.y, particle.radius, particle.radius); 128 | 129 | p.beginShape(); 130 | let pa: Particle | null = particle; 131 | while (pa != null) { 132 | p.curveVertex(pa.position.x, pa.position.y); 133 | pa = pa.parent; 134 | } 135 | p.endShape(); 136 | 137 | break; 138 | } 139 | 140 | p.pop(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /sketches/eyes.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class GooglyEyes implements ISketch { 4 | public readonly name = "Googly Eyes"; 5 | 6 | public readonly width = 800; 7 | public readonly height = 800; 8 | public readonly loop = true; 9 | 10 | private t: number = 0; 11 | private colors: p5.Color[][][] = []; 12 | private r = 0; 13 | private readonly horEyes = 5; 14 | private readonly verEyes = 5; 15 | private readonly subEyes = 4; 16 | 17 | public reset(p: p5) { 18 | p.colorMode(p.HSB); 19 | 20 | for (let x = 0; x < this.horEyes; ++x) { 21 | this.colors[x] = []; 22 | for (let y = 0; y < this.verEyes; ++y) { 23 | this.colors[x][y] = []; 24 | 25 | const h = p.color(p.random(210, 360), 80, 100); 26 | const oh = p.color((180 + p.hue(h)) % 360, 80, 100); 27 | 28 | for (let e = 0; e < this.subEyes; ++e) { 29 | const t = e / (this.subEyes - 1); 30 | this.colors[x][y][e] = p.lerpColor(h, oh, t); 31 | } 32 | } 33 | } 34 | 35 | this.r = Math.min((this.width) / this.horEyes, (this.height) / this.verEyes) - 20; 36 | this.r /= 2; 37 | 38 | } 39 | 40 | public draw(p: p5) { 41 | p.background("#e4572e"); 42 | 43 | const xstep = this.width / this.horEyes; 44 | const ystep = this.height / this.horEyes; 45 | 46 | p.noStroke(); 47 | 48 | for (let x = 0; x < this.horEyes; ++x) { 49 | for (let y = 0; y < this.verEyes; ++y) { 50 | for (let e = 0; e < this.subEyes; ++e) { 51 | p.push(); 52 | 53 | p.translate(x * xstep + xstep / 2, y * ystep + ystep / 2); 54 | 55 | const a = p.noise(x, y, this.t) * p.TWO_PI; 56 | const px = Math.cos(a) * (this.r); 57 | const py = Math.sin(a) * (this.r); 58 | 59 | for (let c = 0; c < this.subEyes; ++c) { 60 | p.fill(this.colors[x][y][c]); 61 | 62 | const cr = this.r * (this.subEyes - 1 - c) / (this.subEyes - 1); 63 | p.ellipse(px - Math.cos(a) * cr, py - Math.sin(a) * cr, cr * 2, cr * 2); 64 | } 65 | 66 | p.pop(); 67 | } 68 | } 69 | } 70 | 71 | this.t += 0.01; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /sketches/focus-eye.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class FocusEye implements ISketch { 4 | public readonly name = "Focus Eye"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | 10 | private t = 0; 11 | private focusPoint = new p5.Vector(0, 0); 12 | 13 | public reset(p: p5) { 14 | p.background("black"); 15 | p.frameRate(10); 16 | 17 | this.focusPoint = p.createVector( 18 | p.random(20, this.width - 20), 19 | p.random(20, this.height - 20), 20 | ); 21 | } 22 | 23 | public draw(p: p5) { 24 | const focusPoint = this.focusPoint; 25 | 26 | p.strokeWeight(0.5); 27 | p.stroke("white"); 28 | p.fill("white"); 29 | 30 | const gridSize = p.random(1, 5); 31 | for (let x = 0; x <= this.width; x += this.width / gridSize) { 32 | this.slowLine(p, p.createVector(x, 0), focusPoint); 33 | this.slowLine(p, p.createVector(x, this.height), focusPoint); 34 | } 35 | for (let y = 0; y <= this.height; y += this.height / gridSize) { 36 | this.slowLine(p, p.createVector(0, y), focusPoint); 37 | this.slowLine(p, p.createVector(this.width, y), focusPoint); 38 | } 39 | 40 | p.noFill(); 41 | p.stroke("white"); 42 | p.strokeWeight(10); 43 | p.rect(0, 0, this.width, this.height); 44 | 45 | const a = p.noise(this.t) * p.TWO_PI; 46 | focusPoint.add(p5.Vector.fromAngle(a).mult(5)); 47 | this.t += 0.01; 48 | } 49 | 50 | private slowLine(p: p5, a: p5.Vector, b: p5.Vector) { 51 | if (p.random() > 0.7) { 52 | return; 53 | } 54 | 55 | const d = b.copy().sub(a); 56 | const l = d.mag(); 57 | d.normalize(); 58 | 59 | const ellipseRad = 1; 60 | 61 | for (let cl = 0; cl < l; cl += ellipseRad) { 62 | const c = a.copy().add(d.copy().mult(cl)); 63 | 64 | const alpha = p.lerp(10, 50, cl / (l - ellipseRad)); 65 | p.fill(255, 255, 255, alpha); 66 | p.stroke(255, 255, 255, alpha); 67 | 68 | p.ellipse(c.x, c.y, ellipseRad * 2, ellipseRad * 2); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sketches/galaxy-map.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class GalaxyMap implements ISketch { 4 | public readonly name = "Galaxy Map"; 5 | 6 | public readonly width = 800; 7 | public readonly height = 800; 8 | public readonly loop = false; 9 | 10 | public readonly maxCurves = 10; 11 | 12 | private t = 0; 13 | 14 | public reset(p: p5) { 15 | p.background(80, 80, 80); 16 | p.frameRate(2); 17 | } 18 | 19 | public draw(p: p5) { 20 | p.background(80, 80, 80); 21 | 22 | this.hyperbola(p); 23 | // this.drawTimesTable(p, this.t); 24 | 25 | const nCurves = p.random(this.maxCurves); 26 | for (let i = 0; i < nCurves; ++i) { 27 | p.push(); 28 | 29 | p.translate(p.random(this.width), p.random(this.height)); 30 | p.rotate(p.random(p.TWO_PI)); 31 | const points = [...sinWave(25, this.width / 49, 100)].map((pt) => { 32 | const a = p.random(p.TWO_PI); 33 | pt[0] += Math.cos(a) * 20; 34 | pt[1] += Math.sin(a) * 20; 35 | return pt; 36 | }); 37 | 38 | this.drawWave( 39 | p, 40 | points, 41 | // Math.floor(p.random(1, points.length / 4)), 42 | 1, 43 | true, 44 | ); 45 | 46 | p.pop(); 47 | } 48 | 49 | this.t += 0.1; 50 | } 51 | 52 | public hyperbola(p: p5) { 53 | const points = [...hyperbolaWave(10, 4, 4)]; 54 | 55 | p.push(); 56 | p.translate(this.width * 0.05, this.height * 0.05); 57 | 58 | this.drawWave(p, points, points.length / 2); 59 | 60 | p.rotate(p.HALF_PI); 61 | this.drawWave(p, points, points.length / 2); 62 | 63 | p.rotate(p.HALF_PI); 64 | this.drawWave(p, points, points.length / 2); 65 | 66 | p.rotate(p.HALF_PI); 67 | this.drawWave(p, points, points.length / 2); 68 | 69 | p.pop(); 70 | } 71 | 72 | public drawTimesTable(p: p5, f: number) { 73 | p.push(); 74 | p.translate(this.width / 2, this.height / 2); 75 | 76 | const points = []; 77 | for (let i = 0; i < 100; ++i) { 78 | points.push([ 79 | Math.cos(Math.PI * 2 / 99 * i) * 400, 80 | Math.sin(Math.PI * 2 / 99 * i) * 400, 81 | ] as [number, number]); 82 | } 83 | 84 | p.stroke(200, 200, 200, 50); 85 | 86 | for (let i = 0; i < points.length; ++i) { 87 | const [x, y] = points[i]; 88 | const [nx, ny] = points[Math.floor(i * f) % points.length]; 89 | 90 | p.line(x, y, nx, ny); 91 | } 92 | 93 | p.pop(); 94 | } 95 | 96 | private drawWave(p: p5, points: Array<[number, number]>, n: number, drawEllipses: boolean = false) { 97 | p.noFill(); 98 | 99 | if (drawEllipses) { 100 | p.stroke(200, 200, 200, 255); 101 | for (const [x, y] of points) { 102 | p.ellipse(x, y, 3); 103 | } 104 | } 105 | 106 | p.stroke(200, 200, 200, 50); 107 | 108 | for (let i = 0; i < points.length - n; ++i) { 109 | const [x, y] = points[i]; 110 | const [nx, ny] = points[i + n]; 111 | 112 | p.line(x, y, nx, ny); 113 | } 114 | } 115 | } 116 | 117 | function* sinWave(steps: number, xstep: number, maxHeight: number): Iterable<[number, number]> { 118 | for (let i = 0; i < steps; ++i) { 119 | yield [ 120 | i * xstep, 121 | Math.sin(Math.PI * 2 / (steps - 1) * i) * maxHeight, 122 | ]; 123 | } 124 | } 125 | 126 | function* hyperbolaWave(steps: number, xstep: number, ystep: number): Iterable<[number, number]> { 127 | for (let i = steps - 1; i >= 0; --i) { 128 | yield [0, i * ystep]; 129 | } 130 | 131 | for (let i = 0; i < steps; ++i) { 132 | yield [i * ystep, 0]; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /sketches/isolines.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class Isolines implements ISketch { 4 | public readonly name = "Isolines"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = false; 9 | 10 | public readonly npoints = 20; 11 | 12 | private t = 0; 13 | private points: [number, number][] = []; 14 | private cx = 0; 15 | private cy = 0; 16 | 17 | private palette = [ 18 | "#0575e6", 19 | "#0394c4", 20 | "#02b3a3", 21 | "#01d281", 22 | "#00f260", 23 | ]; 24 | 25 | public reset(p: p5) { 26 | p.background(0x6, 0x55, 0x107); 27 | 28 | this.cx = p.random(40, this.width - 40); 29 | this.cy = p.random(40, this.height - 40); 30 | 31 | this.points = []; 32 | for (let i = 0; i < this.npoints; ++i) { 33 | let a = i / (this.npoints - 1) * p.TWO_PI; 34 | this.points.push([ 35 | this.cx + Math.cos(a) * 30, 36 | this.cy + Math.sin(a) * 30 37 | ]); 38 | } 39 | 40 | this.points = this.deform(p); 41 | } 42 | 43 | public draw(p: p5) { 44 | let isoLines = []; 45 | for (let i = 0; i < 40; ++i) { 46 | isoLines.push(this.points); 47 | this.points = this.deform(p); 48 | } 49 | isoLines.reverse(); 50 | 51 | p.stroke(0, 0, 0); 52 | 53 | let s = isoLines.length / this.palette.length; 54 | 55 | isoLines.forEach((pts, i) => { 56 | let c = this.palette[Math.floor(i / s)]; 57 | p.fill(c); 58 | 59 | if (i % s == 0) { 60 | p.strokeWeight(3); 61 | } else { 62 | p.strokeWeight(1); 63 | } 64 | 65 | p.beginShape(); 66 | for (const [x, y] of pts) { 67 | p.curveVertex(x, y); 68 | } 69 | for (let i = 1; i < 3; ++i) { 70 | p.curveVertex(pts[i][0], pts[i][1]); 71 | } 72 | p.endShape(); 73 | }); 74 | } 75 | 76 | private deform(p: p5): [number, number][] { 77 | let d: [number, number][] = this.points.map(pt => { 78 | let [x, y] = pt; 79 | 80 | let da = p.map( 81 | p.noise(x, y, this.t), 82 | 0, 1, 83 | -p.QUARTER_PI / 4, p.QUARTER_PI / 4 84 | ); 85 | let a = Math.atan2(y - this.cy, x - this.cx) + da; 86 | let r = p.random(20, 40); 87 | 88 | return [x + Math.cos(a) * r, y + Math.sin(a) * r]; 89 | }); 90 | 91 | this.t += 0.001; 92 | 93 | return d; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /sketches/light-in-a-cave.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class LightInACave implements ISketch { 4 | public readonly name = "Light in a Cave"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | 10 | private maxIterations = 0; 11 | private it = 0; 12 | 13 | public reset(p: p5) { 14 | p.background("black"); 15 | p.stroke(p.random() * 255, p.random() * 255, p.random() * 255, 30); 16 | 17 | this.maxIterations = p.random(600, 800); 18 | this.it = 0; 19 | } 20 | 21 | public draw(p: p5) { 22 | if (this.it++ >= this.maxIterations) { 23 | return; 24 | } 25 | 26 | p.translate(this.width / 2, this.height / 2); 27 | 28 | this.drawSpiral(p); 29 | } 30 | 31 | private drawSpiral(p: p5) { 32 | p.noFill(); 33 | 34 | let radius = 10; 35 | let angle = 0; 36 | 37 | const angleFac = p.random(2, 7); 38 | 39 | p.beginShape(); 40 | 41 | while (true) { 42 | 43 | const x = Math.cos(angle) * radius; 44 | const y = Math.sin(angle) * radius; 45 | 46 | if (x <= -this.width / 2 || x >= this.width / 2 || y <= -this.height / 2 || y >= this.height / 2) { 47 | break; 48 | } 49 | 50 | p.vertex(x, y); 51 | 52 | radius += p.random(radius / 4); 53 | angle += p.PI / angleFac / (p.random(4) + 1); 54 | } 55 | 56 | p.endShape(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sketches/neon-lines.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class NeonLines implements ISketch { 4 | public readonly name = "Neon Lines"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | 10 | public readonly gridSize = 40; 11 | 12 | public backgroundColor: p5.Color = "cannot happen" as unknown as p5.Color; 13 | 14 | public reset(p: p5) { 15 | this.backgroundColor = p.color("rgb(0,46,99)"); 16 | p.background(this.backgroundColor); 17 | 18 | p.frameRate(3); 19 | } 20 | 21 | public draw(p: p5) { 22 | this.drawLine(p, this.backgroundColor); 23 | } 24 | 25 | private drawLine(p: p5, backgroundColor: p5.Color) { 26 | p.strokeWeight(5); 27 | 28 | const c = p.color( 29 | p.red(backgroundColor) + p.random(255 - p.red(backgroundColor)), 30 | p.blue(backgroundColor) + p.random(255 - p.blue(backgroundColor)), 31 | p.green(backgroundColor) + p.random(255 - p.green(backgroundColor)), 32 | ); 33 | p.stroke(c); 34 | p.fill(c); 35 | p.strokeCap(p.PROJECT); 36 | 37 | let prev: [number, number] | null = null; 38 | 39 | for (const [x, y] of this.randomWalk(p, p.random(10, 50))) { 40 | if (prev !== null) { 41 | p.line(prev[0], prev[1], x, y); 42 | } 43 | 44 | prev = [x, y]; 45 | } 46 | 47 | if (prev !== null) { 48 | if (p.random() > 0.5) { 49 | p.noFill(); 50 | 51 | p.strokeWeight(p.random(3, 10)); 52 | 53 | const s = p.random(50, 150); 54 | p.ellipse(prev[0], prev[1], s, s); 55 | } else { 56 | p.ellipse(prev[0], prev[1], 30, 30); 57 | } 58 | } 59 | } 60 | 61 | private * randomWalk(p: p5, n: number): Iterable<[number, number]> { 62 | const cellWidth = this.width / this.gridSize; 63 | const cellHeight = this.height / this.gridSize; 64 | 65 | // start on the left or top edge and walk towards bottom right 66 | let startx = 0; 67 | let starty = 0; 68 | if (p.random() > 0.5) { 69 | startx = Math.floor(p.random(0, this.gridSize)) * cellWidth; 70 | } else { 71 | starty = Math.floor(p.random(0, this.gridSize)) * cellHeight; 72 | } 73 | 74 | const pt: [number, number] = [startx, starty]; 75 | 76 | for (let i = 0; i < n; ++i) { 77 | yield pt; 78 | 79 | if (p.random() > 0.2) { 80 | pt[0] += cellWidth; 81 | if (pt[0] >= this.width) { 82 | break; 83 | } 84 | } 85 | 86 | if (p.random() > 0.2) { 87 | pt[1] += p.random([cellHeight]); 88 | if (pt[1] >= this.height) { 89 | break; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /sketches/noise-quads.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class NoiseQuads implements ISketch { 4 | public readonly name = "Noise Quads"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | 10 | private t = 0; 11 | private n = 0; 12 | private padding = 20; 13 | 14 | public reset(p: p5) { 15 | p.background("white"); 16 | 17 | this.t += p.random(); 18 | this.n = Math.floor(p.random(1, 5)); 19 | 20 | p.strokeWeight(10); 21 | p.rect(0, 0, this.width, this.height); 22 | p.strokeWeight(1); 23 | } 24 | 25 | public draw(p: p5) { 26 | p.push(); 27 | p.translate(this.padding, this.padding); 28 | 29 | p.rectMode(p.CENTER); 30 | 31 | const s = Math.min(this.width - this.padding * 2, this.height - this.padding * 2) / this.n; 32 | 33 | const w = this.width - this.padding * 2; 34 | const h = this.height - this.padding * 2; 35 | const wpad = (w - Math.floor(w / s) * s) / 2; 36 | const hpad = (h - Math.floor(h / s) * s) / 2; 37 | 38 | p.stroke("black"); 39 | p.fill("white"); 40 | 41 | for (let x = wpad; x <= this.width - s - this.padding * 2 - wpad; x += s) { 42 | for (let y = hpad; y <= this.height - s - this.padding * 2 - hpad; y += s) { 43 | const nt = p.noise(x, y, this.t); 44 | const l = s * nt; 45 | 46 | if (p.random() > 0.3) { 47 | p.noFill(); 48 | } else { 49 | p.fill("white"); 50 | } 51 | 52 | p.push(); 53 | p.translate(x + s / 2, y + s / 2); 54 | // p.rotate(p.PI * nt); 55 | p.rect(0, 0, l, l); 56 | p.pop(); 57 | } 58 | } 59 | 60 | this.n += 3; 61 | 62 | p.frameRate(1); 63 | // p.noLoop(); 64 | 65 | p.pop(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /sketches/nucleus.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | import { randomPointInCircle, sampleCircle } from "./utils"; 3 | 4 | export class Nucleus implements ISketch { 5 | public readonly name = "Nucleus"; 6 | 7 | public readonly width = 600; 8 | public readonly height = 600; 9 | public readonly loop = false; 10 | 11 | public readonly circleDeformations = 15; 12 | public readonly perturbations = 10; 13 | 14 | public reset(p: p5) { 15 | p.background("white"); 16 | } 17 | 18 | public draw(p: p5) { 19 | p.translate(this.width / 2, this.height / 2); 20 | this.drawDrop(p); 21 | } 22 | 23 | private drawDrop(p: p5) { 24 | if (p.random() > 0.5) { 25 | p.blendMode(p.EXCLUSION); 26 | } 27 | 28 | p.fill(p.random(255), p.random(255), p.random(255), 10); 29 | p.stroke(0, 0, 0, 30); 30 | 31 | for (let i = 0; i < this.circleDeformations; ++i) { 32 | let polylinePoints: p5.Vector[] = []; 33 | 34 | for (const [x, y] of sampleCircle(p.PI / 8, Math.min(this.width, this.height) / 5)) { 35 | polylinePoints.push(p.createVector(x, y)); 36 | } 37 | 38 | polylinePoints = this.irregularize(p, polylinePoints, 30); 39 | 40 | for (let j = 0; j < this.perturbations; ++j) { 41 | polylinePoints = this.irregularize(p, polylinePoints); 42 | this.drawPoly(p, polylinePoints); 43 | } 44 | } 45 | } 46 | 47 | private drawPoly(p: p5, polylinePoints: p5.Vector[]) { 48 | p.beginShape(); 49 | for (const p0 of polylinePoints) { 50 | p.vertex(p0.x, p0.y); 51 | } 52 | p.endShape(); 53 | } 54 | 55 | private irregularize(p: p5, polylinePoints: p5.Vector[], minEdgeLength?: number): p5.Vector[] { 56 | const out: p5.Vector[] = []; 57 | 58 | for (let i = 0; i < polylinePoints.length; ++i) { 59 | const p0 = polylinePoints[i]; 60 | const p1 = polylinePoints[(i + 1) % polylinePoints.length]; 61 | 62 | out.push(p0); 63 | // p1 is pushed in next iter 64 | 65 | const d = p0.dist(p1); 66 | 67 | if (minEdgeLength !== undefined && d < minEdgeLength) { 68 | continue; 69 | } 70 | 71 | const pt = randomPointInCircle(p, d); 72 | const mid = p5.Vector.add(p0, p1).div(2); 73 | 74 | out.push(mid.add(pt[0], pt[1])); 75 | } 76 | 77 | return out; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sketches/penrose-tiling.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | const goldenRatio = (1 + Math.sqrt(5)) / 2; 4 | 5 | interface Triangle { 6 | a: p5.Vector; 7 | b: p5.Vector; 8 | c: p5.Vector; 9 | big: boolean; 10 | } 11 | 12 | export class PenroseTiling implements ISketch { 13 | public readonly name = "Penrose Tiling"; 14 | 15 | public readonly width = 1920; 16 | public readonly height = 1080; 17 | public readonly loop = true; 18 | 19 | private triangles: Triangle[] = []; 20 | 21 | public reset(p: p5) { 22 | p.frameRate(1); 23 | 24 | p.background("white"); 25 | 26 | this.triangles = []; 27 | 28 | for (let i = 0; i < 10; ++i) { 29 | const b = p5.Vector.fromAngle((2 * i - 1) * Math.PI / 10); 30 | const c = p5.Vector.fromAngle((2 * i + 1) * Math.PI / 10); 31 | 32 | if (i % 2 == 0) { 33 | this.triangles.push({ 34 | big: false, 35 | a: p.createVector(), 36 | b: c, 37 | c: b, 38 | }); 39 | } else { 40 | this.triangles.push({ 41 | big: false, 42 | a: p.createVector(), 43 | b, 44 | c, 45 | }); 46 | } 47 | } 48 | } 49 | 50 | public draw(p: p5) { 51 | p.push(); 52 | p.background("white"); 53 | p.translate(this.width / 2, this.height / 2); 54 | 55 | const scale = 1.2 * Math.sqrt(Math.pow(this.width / 2, 2) + Math.pow(this.height / 2, 2)); 56 | 57 | for (const tri of this.triangles) { 58 | p.noStroke(); 59 | if (tri.big) { 60 | p.fill(p.color(102, 102, 255)); 61 | } else { 62 | p.fill(p.color(255, 90, 90)); 63 | } 64 | 65 | p.beginShape(); 66 | p.vertex(tri.a.x * scale, tri.a.y * scale); 67 | p.vertex(tri.b.x * scale, tri.b.y * scale); 68 | p.vertex(tri.c.x * scale, tri.c.y * scale); 69 | p.endShape(p.CLOSE); 70 | 71 | p.stroke(p.color(51, 51, 51)); 72 | p.strokeWeight(3); 73 | p.noFill(); 74 | p.line(tri.c.x * scale, tri.c.y * scale, tri.a.x * scale, tri.a.y * scale); 75 | p.line(tri.a.x * scale, tri.a.y * scale, tri.b.x * scale, tri.b.y * scale); 76 | } 77 | p.pop(); 78 | 79 | if (this.triangles.length < 100000) { 80 | this.triangles = this.subdivide(this.triangles); 81 | } 82 | } 83 | 84 | private subdivide(triangles: Triangle[]): Triangle[] { 85 | const newTriangles = []; 86 | 87 | for (const tri of triangles) { 88 | if (tri.big) { 89 | const q = tri.b.copy().add(tri.a.copy().sub(tri.b).div(goldenRatio)); // Q = B + (A - B) / goldenRatio 90 | const r = tri.b.copy().add(tri.c.copy().sub(tri.b).div(goldenRatio)); // R = B + (C - B) / goldenRatio 91 | 92 | newTriangles.push( 93 | { big: true, a: r, b: tri.c, c: tri.a }, 94 | { big: true, a: q, b: r, c: tri.b }, 95 | { big: false, a: r, b: q, c: tri.a }, 96 | ); 97 | } else { 98 | const p = tri.a.copy().add(tri.b.copy().sub(tri.a).div(goldenRatio)); 99 | newTriangles.push( 100 | { big: false, a: tri.c, b: p, c: tri.b }, 101 | { big: true, a: p, b: tri.c, c: tri.a }, 102 | ); 103 | } 104 | } 105 | 106 | return newTriangles; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /sketches/print10.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | // https://10print.org/ 4 | export class Print10 implements ISketch { 5 | public readonly name = "10Print"; 6 | 7 | public readonly width = 600; 8 | public readonly height = 600; 9 | public readonly loop = false; 10 | 11 | // this should be configurable from a ui 12 | public readonly step = 20; 13 | 14 | public reset(p: p5) { 15 | p.background("white"); 16 | } 17 | 18 | public draw(p: p5) { 19 | p.strokeWeight(3); 20 | 21 | for (let x = 0; x < this.width; x += this.step) { 22 | for (let y = 0; y < this.height; y += this.step) { 23 | 24 | if (Math.random() < 0.5) { 25 | p.line(x, y, x + this.step, y + this.step); 26 | } else { 27 | p.line(x + this.step, y, x, y + this.step); 28 | } 29 | 30 | } 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /sketches/roses.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | type Point = [number, number]; 4 | type Shape = Point[]; 5 | 6 | export class Roses implements ISketch { 7 | public readonly name = "Roses"; 8 | 9 | public readonly width = 1920; 10 | public readonly height = 1080; 11 | public readonly loop = true; 12 | 13 | private readonly npoints = 40; 14 | private shapes: Shape[] = []; 15 | private t = 0; 16 | 17 | public reset(p: p5) { 18 | p.background("black"); 19 | p.frameRate(10); 20 | 21 | const genCircle = (r: number) => { 22 | let shape: Shape = []; 23 | for (let i = 0; i < this.npoints; ++i) { 24 | let a = i / (this.npoints - 1) * p.TWO_PI; 25 | shape.push([ 26 | Math.cos(a) * r, 27 | Math.sin(a) * r 28 | ]); 29 | } 30 | return shape; 31 | }; 32 | 33 | let r = Math.min(this.height, this.width) / 3; 34 | this.shapes = [ 35 | // genCircle(r), 36 | // genCircle(r * 0.75), 37 | // genCircle(r * 0.50), 38 | genCircle(r * 0.25), 39 | genCircle(r * 0.2), 40 | ]; 41 | } 42 | 43 | public draw(p: p5) { 44 | let mutated: Shape[] = []; 45 | 46 | for (let si = 0; si < this.shapes.length; ++si) { 47 | const s = this.shapes[si]; 48 | 49 | for (let g = 0; g < 10; ++g) { 50 | mutated[si] = s.map(([x, y]) => { 51 | let f = p.noise(x, y, this.t); 52 | 53 | let a = Math.atan2(y, x); 54 | let da = p.map(f, 0, 1, -p.QUARTER_PI/4, p.QUARTER_PI/4); 55 | let r = p.map(f, 0, 1, 20, 80); 56 | 57 | return [x + Math.cos(a + da) * r, y + Math.sin(a + da) * r]; 58 | }); 59 | 60 | p.push(); 61 | p.translate(this.width / 2, this.height / 2); 62 | p.stroke("#c21e560a"); 63 | p.noFill(); 64 | 65 | p.beginShape(); 66 | for (const [x, y] of mutated[si]) { 67 | p.curveVertex(x, y); 68 | } 69 | for (let i = 1; i < 3; ++i) { 70 | p.curveVertex(mutated[si][i][0], mutated[si][i][1]); 71 | } 72 | p.endShape(); 73 | p.pop(); 74 | 75 | this.t += 0.01; 76 | } 77 | 78 | if (p.random() > 0.9) { 79 | this.shapes = mutated; 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /sketches/rots.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class Rots implements ISketch { 4 | public readonly name = "Rots"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | 10 | public reset(p: p5) { 11 | p.background("white"); 12 | } 13 | 14 | public draw(p: p5) { 15 | p.background("white"); 16 | 17 | const rows = 4; 18 | const columns = 7; 19 | const cellW = this.width / columns; 20 | const cellH = this.height / rows; 21 | const t = p.random(0.05, 0.5); 22 | 23 | for (let r = 0; r < rows; ++r) { 24 | for (let c = 0; c < 10; ++c) { 25 | p.push(); 26 | p.translate(c * cellW + cellW / 2, r * cellH + cellH / 2); 27 | p.rotate(p.random(p.TWO_PI)); 28 | 29 | this.drawRots(p, 0.95 * Math.min(cellW / 2, cellH / 2), t); 30 | 31 | p.pop(); 32 | } 33 | } 34 | 35 | p.frameRate(0.25); 36 | } 37 | 38 | private drawRots(p: p5, r: number, t: number) { 39 | const nVertices = Math.floor(p.random(3, 8)); 40 | 41 | let vertices: p5.Vector[] = []; 42 | for (let i = 0; i < nVertices; ++i) { 43 | vertices.push(p5.Vector.fromAngle((i / nVertices) * p.TWO_PI, r)); 44 | } 45 | 46 | const drawVertices = (t: number) => { 47 | p.beginShape(); 48 | for (const { x, y } of vertices) { 49 | p.vertex(x, y); 50 | } 51 | p.endShape(p.CLOSE); 52 | }; 53 | 54 | drawVertices(0); 55 | 56 | const n = Math.floor(p.random(2, 10)); 57 | 58 | for (let i = 0; i < n; ++i) { 59 | const newVertices = []; 60 | 61 | for (let j = 0; j < vertices.length; ++j) { 62 | newVertices.push( 63 | p5.Vector.lerp(vertices[j], vertices[(j + 1) % vertices.length], t), 64 | ); 65 | } 66 | 67 | vertices = newVertices; 68 | drawVertices((i + 1) / n); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sketches/rough-balls.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class RoughBalls implements ISketch { 4 | public readonly name = "Rough Balls"; 5 | 6 | public readonly width = 1300; 7 | public readonly height = 800; 8 | public readonly loop = false; 9 | 10 | public readonly radius = 200; 11 | 12 | public reset(p: p5) { 13 | p.background("white"); 14 | } 15 | 16 | public draw(p: p5) { 17 | p.strokeWeight(5); 18 | 19 | const cx = this.width / 2; 20 | 21 | const balls = [ 22 | { 23 | alpha: 10, 24 | iterations: 500, 25 | radius: this.radius / 4, 26 | xs: [cx - this.width / 12, cx, cx + this.width / 12], 27 | y: this.height / 8, 28 | }, 29 | { 30 | alpha: 15, 31 | iterations: 1000, 32 | radius: this.radius / 2, 33 | xs: [cx - this.width / 6, cx, cx + this.width / 6], 34 | y: this.height / 8 * 3, 35 | }, 36 | { 37 | alpha: 15, 38 | iterations: 5000, 39 | radius: this.radius, 40 | xs: [cx - this.width / 3, cx, cx + this.width / 3], 41 | y: this.height / 8 * 3 + this.height / 3, 42 | }, 43 | ]; 44 | 45 | for (const { radius, y, xs, iterations } of balls) { 46 | const pa = p.random(); 47 | 48 | let planetsInRow = 1; 49 | if (pa >= 0.9) { 50 | planetsInRow = 3; 51 | } else if (pa >= 0.8) { 52 | planetsInRow = 2; 53 | } 54 | 55 | for (let i = 0; i < planetsInRow; ++i) { 56 | const x = p.random(xs); 57 | p.push(); 58 | 59 | p.translate(x, y); 60 | this.drawBall(p, radius, iterations, 10); 61 | 62 | p.pop(); 63 | } 64 | } 65 | } 66 | 67 | public drawBall(p: p5, radius: number, iterations: number, alpha: number) { 68 | for (let i = 0; i < iterations; ++i) { 69 | p.stroke(p.random(255), p.random(255), p.random(255), alpha); 70 | p.noFill(); 71 | 72 | const a = p.random(p.TWO_PI); 73 | const sx = Math.cos(a) * radius; 74 | const sy = Math.sin(a) * radius; 75 | 76 | const ea = p.random(p.TWO_PI); 77 | const ex = Math.cos(ea) * radius; 78 | const ey = Math.sin(ea) * radius; 79 | 80 | const midx = (sx + ex) / 2; 81 | const midy = (sy + ey) / 2; 82 | 83 | p.strokeWeight((midx * midx + midy * midy) / (radius * radius) * 3); 84 | 85 | p.bezier( 86 | sx, sy, 87 | midx + p.random(-0.5, 0.5) * radius / 2, midy + p.random(-0.5, 0.5) * radius / 2, 88 | midx + p.random(-0.5, 0.5) * radius / 2, midy + p.random(-0.5, 0.5) * radius / 2, 89 | ex, ey, 90 | ); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /sketches/scribbles.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | interface Particle { 4 | position: p5.Vector; 5 | velocity: p5.Vector; 6 | acceleration: number; 7 | angle: number; 8 | path: p5.Vector[]; 9 | alive: boolean; 10 | } 11 | 12 | interface Container { 13 | contains: (x: number, y: number) => boolean; 14 | random: (p: p5) => [number, number]; 15 | } 16 | 17 | class Rect implements Container { 18 | constructor( 19 | public readonly x: number, 20 | public readonly y: number, 21 | public readonly width: number, 22 | public readonly height: number) {} 23 | 24 | public contains(x: number, y: number): boolean { 25 | return this.x <= x && this.y <= y && this.x + this.width >= x && this.y + this.height >= y; 26 | } 27 | 28 | public random(p: p5): [number, number] { 29 | return [p.random(this.x, this.x + this.width), p.random(this.y, this.y + this.height)]; 30 | } 31 | } 32 | 33 | class Circle implements Container { 34 | constructor( 35 | public readonly x: number, 36 | public readonly y: number, 37 | public readonly radius: number) {} 38 | 39 | public contains(x: number, y: number): boolean { 40 | return Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2) <= Math.pow(this.radius, 2); 41 | } 42 | 43 | public random(p: p5): [number, number] { 44 | const a = p.random(p.TWO_PI); 45 | const r = p.random(this.radius); 46 | 47 | return [this.x + Math.cos(a) * r, this.y + Math.sin(a) * r]; 48 | } 49 | } 50 | 51 | class Triangle implements Container { 52 | constructor( 53 | public readonly a: p5.Vector, 54 | public readonly b: p5.Vector, 55 | public readonly c: p5.Vector) {} 56 | 57 | public contains(x: number, y: number): boolean { 58 | const sign = (p1: p5.Vector, p2: p5.Vector, p3: p5.Vector) => { 59 | return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); 60 | }; 61 | 62 | const xy = this.a.copy(); 63 | xy.set(x, y); 64 | 65 | const d1 = sign(xy, this.a, this.b); 66 | const d2 = sign(xy, this.b, this.c); 67 | const d3 = sign(xy, this.c, this.a); 68 | 69 | const has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0); 70 | const has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0); 71 | 72 | return !(has_neg && has_pos); 73 | } 74 | 75 | public random(p: p5): [number, number] { 76 | const r1 = p.random(); 77 | const r2 = p.random(); 78 | 79 | const pt = this.a.copy().mult(1 - Math.sqrt(r1)) 80 | .add(this.b.copy().mult((1 - r2) * Math.sqrt(r1))) 81 | .add(this.c.copy().mult(r2 * Math.sqrt(r1))); 82 | 83 | return [pt.x, pt.y]; 84 | } 85 | } 86 | 87 | export class Scribbles implements ISketch { 88 | public readonly name = "Scribbles"; 89 | 90 | public readonly width = 1920; 91 | public readonly height = 1080; 92 | public readonly loop = true; 93 | 94 | private particles: Particle[] = []; 95 | private container: Container = new Rect(0,0,0,0); 96 | 97 | public reset(p: p5) { 98 | p.colorMode(p.HSB); 99 | 100 | const cp = p.random(); 101 | 102 | if (cp < 0.33) { 103 | this.container = new Rect(0, 0, this.width, this.height); 104 | } else if (cp < 0.66) { 105 | this.container = new Circle(this.width/2, this.height/2, 300); 106 | } else { 107 | const t = p.createVector(p.random(this.width/2), p.random(this.height/2)); 108 | const l = p.createVector(p.random(this.width/2, this.width), 109 | p.random(this.height/2)); 110 | // c = (t + l + r) / 3 => 111 | // 3*c = t + l + r => 112 | // r = 3*c - t - l 113 | const r = p.createVector(this.width/2, this.height/2).mult(3).sub(t).sub(l); 114 | this.container = new Triangle(t, l, r); 115 | } 116 | 117 | this.particles = []; 118 | 119 | const n = p.random(1, 9); 120 | for (let i = 0; i < n; ++i) { 121 | this.particles.push(this.genParticle(p, ...this.container.random(p))); 122 | } 123 | } 124 | 125 | public draw(p: p5) { 126 | p.background("white"); 127 | p.stroke(0, 0, 0); 128 | p.noFill(); 129 | 130 | p.strokeWeight(5); 131 | p.rect(0, 0, this.width, this.height); 132 | p.strokeWeight(1); 133 | 134 | for (const particle of this.particles) { 135 | if (particle.alive && !this.container.contains(particle.position.x, particle.position.y)) { 136 | particle.alive = false; 137 | this.particles.push(this.genParticle(p, ...this.container.random(p))); 138 | } 139 | 140 | if (particle.alive) { 141 | particle.path.push(particle.position.copy()); 142 | 143 | particle.acceleration += p.random(-0.01, 0.011); 144 | particle.angle = p.randomGaussian(0, 1); 145 | 146 | particle.velocity.add(particle.acceleration).rotate(particle.angle); 147 | particle.velocity.limit(10); 148 | 149 | particle.position.add(particle.velocity); 150 | } 151 | 152 | p.beginShape(); 153 | for (const pos of particle.path) { 154 | p.curveVertex(pos.x, pos.y); 155 | } 156 | p.endShape(); 157 | 158 | } 159 | } 160 | 161 | private genParticle(p: p5, x: number, y: number): Particle { 162 | return { 163 | acceleration: p.random(), 164 | alive: true, 165 | angle: p.random(p.TWO_PI), 166 | path: [], 167 | position: p.createVector(x, y), 168 | velocity: p5.Vector.random2D(), 169 | }; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /sketches/sketch.ts: -------------------------------------------------------------------------------- 1 | export interface ISketch { 2 | name: string; 3 | 4 | width: number; 5 | height: number; 6 | 7 | loop: boolean; 8 | 9 | reset(p: p5): void; 10 | draw(p: p5): void; 11 | } 12 | -------------------------------------------------------------------------------- /sketches/sorting.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | type SortingAlgo = (arr: number[]) => Iterable; 4 | 5 | export class Sorting implements ISketch { 6 | public readonly name = "Sorting Algorithms"; 7 | 8 | public readonly width = 1920; 9 | public readonly height = 1080; 10 | public readonly loop = false; 11 | 12 | private sortAlgo: SortingAlgo = selectionSort; 13 | 14 | public reset(p: p5) { 15 | p.background("white"); 16 | 17 | this.sortAlgo = p.random([selectionSort, bubbleSort, quickSort]); 18 | } 19 | 20 | public draw(p: p5) { 21 | const n = Math.floor(p.random(2, 150)); 22 | 23 | const numbers: number[] = []; 24 | for (let i = 0; i < n; ++i) { 25 | numbers.push(p.random()); 26 | } 27 | 28 | const passes = [numbers.slice()]; 29 | for (const step of this.sortAlgo(numbers)) { 30 | passes.push(step.slice()); 31 | } 32 | 33 | for (let i = 0; i < passes[passes.length - 1].length - 1; ++i) { 34 | if (passes[passes.length - 1][i] > passes[passes.length - 1][i + 1]) { 35 | console.warn("not sorted", this.sortAlgo.toString()); 36 | } 37 | } 38 | 39 | p.noStroke(); 40 | p.colorMode(p.HSB); 41 | const h1 = p.random(360); 42 | const h2 = (h1 + 180) % 360; 43 | 44 | const h = this.height / passes.length; 45 | const w = this.width / n; 46 | for (let pa = 0; pa < passes.length; ++pa) { 47 | const pass = passes[pa]; 48 | 49 | for (let i = 0; i < pass.length; ++i) { 50 | p.fill(p.lerp(h1, h2, pass[i]), 80, 80); 51 | p.rect(w * i, pa * h, w + 1, h + 1); 52 | } 53 | } 54 | } 55 | } 56 | 57 | function* selectionSort(numbers: number[]): Iterable { 58 | for (let i = 0; i < numbers.length - 1; i++) { 59 | let jMin = i; 60 | 61 | for (let j = i + 1; j < numbers.length; j++) { 62 | if (numbers[j] < numbers[jMin]) { 63 | jMin = j; 64 | } 65 | } 66 | 67 | if (jMin != i) { 68 | swap(numbers, i, jMin); 69 | } 70 | 71 | yield numbers; 72 | } 73 | } 74 | 75 | function* bubbleSort(numbers: number[]): Iterable { 76 | let n = numbers.length; 77 | 78 | do { 79 | let newn = 0; 80 | for (let i = 1; i < n; ++i) { 81 | if (numbers[i - 1] > numbers[i]) { 82 | swap(numbers, i - 1, i); 83 | newn = i; 84 | } 85 | } 86 | n = newn; 87 | 88 | yield numbers; 89 | } while (n > 1); 90 | } 91 | 92 | function* quickSort(numbers: number[], left: number = 0, right?: number): Iterable { 93 | right = right === undefined ? numbers.length - 1 : right; 94 | 95 | if (left >= right) { 96 | yield numbers; 97 | return; 98 | } 99 | 100 | const partition = (pivot: number, left: number, right: number) => { 101 | const pivotValue = numbers[pivot]; 102 | 103 | let partitionIndex = left; 104 | 105 | for (let i = left; i < right; i++) { 106 | if (numbers[i] < pivotValue) { 107 | swap(numbers, i, partitionIndex); 108 | partitionIndex++; 109 | } 110 | } 111 | 112 | swap(numbers, right, partitionIndex); 113 | return partitionIndex; 114 | }; 115 | 116 | const pivot = right; 117 | const partitionIndex = partition(pivot, left, right); 118 | 119 | yield numbers; 120 | 121 | yield* quickSort(numbers, left, partitionIndex - 1); 122 | yield* quickSort(numbers, partitionIndex + 1, right); 123 | } 124 | 125 | function swap(numbers: number[], i: number, j: number) { 126 | const tmp = numbers[i]; 127 | numbers[i] = numbers[j]; 128 | numbers[j] = tmp; 129 | } 130 | -------------------------------------------------------------------------------- /sketches/space-filling-curves.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class SpaceFillingCurves implements ISketch { 4 | public readonly name = "Space Filling Curves"; 5 | 6 | public readonly width = 800; 7 | public readonly height = 800; 8 | public readonly loop = false; 9 | 10 | private readonly padding = 10; 11 | 12 | public reset(p: p5) { 13 | p.background(80, 80, 80); 14 | } 15 | 16 | public draw(p: p5) { 17 | p.stroke(200, 200, 200); 18 | 19 | const size = this.width / 2 - 2 * this.padding; 20 | 21 | p.push(); 22 | p.translate(this.padding, this.padding); 23 | this.drawQuad(p, size, 1); 24 | p.pop(); 25 | 26 | p.push(); 27 | p.translate(this.width / 2 + this.padding, this.padding); 28 | this.drawQuad(p, size, 1); 29 | p.pop(); 30 | 31 | p.push(); 32 | p.translate(this.padding, this.height / 2 + this.padding); 33 | this.drawQuad(p, size, 1); 34 | p.pop(); 35 | 36 | p.push(); 37 | p.translate(this.width / 2 + this.padding, this.height / 2 + this.padding); 38 | this.drawQuad(p, size, 1); 39 | p.pop(); 40 | } 41 | 42 | private drawQuad(p: p5, size: number, depth: number) { 43 | if (p.random() <= depth / 3) { 44 | const curves = [ 45 | new HilbertCurve(), 46 | new HilbertCurve2(), 47 | new PeanoGosperCurve(), 48 | ]; 49 | 50 | const c: Curve = p.random(curves); 51 | const gens = c.randGen(depth); 52 | 53 | // PeanoGosperCurve doesn't start at the top left, adjust translation 54 | if (c instanceof PeanoGosperCurve) { 55 | const sl = c.strokeLen(gens, size); 56 | const hl = Math.sqrt(3) / 2 * sl; 57 | 58 | // I wasn't able to figure out the math to properly translate 59 | // the curve, eyeball it. |- T -| 60 | p.translate( 61 | hl * ((depth === 1) ? 14 : 2), 62 | hl * ((depth === 1) ? 14 : 7), 63 | ); 64 | } 65 | 66 | this.drawLSystem(p, c.advance(gens), c.strokeLen(gens, size)); 67 | return; 68 | } 69 | 70 | const np = this.padding / depth; 71 | const s = size / 2 - np * 2; 72 | 73 | p.push(); 74 | p.translate(np, np); 75 | this.drawQuad(p, s, depth + 1); 76 | p.pop(); 77 | 78 | p.push(); 79 | p.translate(size / 2 + np, np); 80 | this.drawQuad(p, s, depth + 1); 81 | p.pop(); 82 | 83 | p.push(); 84 | p.translate(np, size / 2 + np); 85 | this.drawQuad(p, s, depth + 1); 86 | p.pop(); 87 | 88 | p.push(); 89 | p.translate(size / 2 + np, size / 2 + np); 90 | this.drawQuad(p, s, depth + 1); 91 | p.pop(); 92 | } 93 | 94 | private drawLSystem(p: p5, lSys: LSystem, w: number) { 95 | for (const c of lSys.state) { 96 | switch (c) { 97 | case "+": 98 | p.rotate(lSys.theta); 99 | break; 100 | case "-": 101 | p.rotate(-lSys.theta); 102 | break; 103 | case "F": 104 | p.line(0, 0, w, 0); 105 | p.translate(w, 0); 106 | break; 107 | } 108 | } 109 | } 110 | } 111 | 112 | export class LSystem { 113 | constructor( 114 | public readonly state: string, 115 | public readonly rules: Map, 116 | public readonly theta: number = Math.PI / 2, 117 | ) { } 118 | 119 | public advance(gens: number = 1): LSystem { 120 | if (gens <= 0) { 121 | return this; 122 | } 123 | 124 | let newState = ""; 125 | 126 | for (const c of this.state) { 127 | let nc = this.rules.get(c); 128 | if (nc === undefined) { 129 | nc = c; 130 | } 131 | 132 | newState += nc; 133 | 134 | } 135 | 136 | const lSys = new LSystem(newState, this.rules, this.theta); 137 | return lSys.advance(gens - 1); 138 | } 139 | } 140 | 141 | export type Curve = HilbertCurve | HilbertCurve2 | PeanoGosperCurve; 142 | 143 | export class HilbertCurve extends LSystem { 144 | constructor() { 145 | super("L", new Map([ 146 | ["L", "+RF-LFL-FR+"], 147 | ["R", "-LF+RFR+FL-"], 148 | ])); 149 | } 150 | 151 | public strokeLen(gens: number, size: number): number { 152 | return size / (Math.pow(2, gens) - 1); 153 | } 154 | 155 | public randGen(depth: number): number { 156 | return Math.floor(2 + Math.random() * (7 - depth)); 157 | } 158 | } 159 | 160 | export class HilbertCurve2 extends LSystem { 161 | constructor() { 162 | super("X", new Map([ 163 | ["X", "XFYFX+F+YFXFY-F-XFYFX"], 164 | ["Y", "YFXFY-F-XFYFX+F+YFXFY"], 165 | ])); 166 | } 167 | 168 | public strokeLen(gens: number, size: number): number { 169 | return size / (Math.pow(3, gens) - 1); 170 | } 171 | 172 | public randGen(depth: number): number { 173 | return Math.floor(2 + Math.random() * (4 - depth)); 174 | } 175 | } 176 | 177 | export class PeanoGosperCurve extends LSystem { 178 | constructor() { 179 | super( 180 | "Y", 181 | new Map([ 182 | ["X", "X+YF++YF-FX--FXFX-YF+"], 183 | ["Y", "-FX+YFYF++YF+FX--FX-Y"], 184 | ]), 185 | Math.PI / 3, 186 | ); 187 | } 188 | 189 | public strokeLen(gens: number, size: number): number { 190 | return size / (Math.pow(2.82, gens) - 1); 191 | } 192 | 193 | public randGen(depth: number): number { 194 | return 3 + Math.max(0, 2 - depth); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /sketches/spiral-christmas-tree.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | // heavily inspired by https://github.com/anvaka/atree 4 | export class ChristmasSpiralTree implements ISketch { 5 | public readonly name = "Christmas Spiral Tree"; 6 | 7 | public readonly width = 600; 8 | public readonly height = 600; 9 | public readonly loop = true; 10 | 11 | private startAngle = 0; 12 | 13 | public reset(p: p5) { 14 | p.background("black"); 15 | p.frameRate(40); 16 | } 17 | 18 | public draw(p: p5) { 19 | p.background("black"); 20 | 21 | p.translate(this.width / 2, this.height); 22 | 23 | const spirals = [ 24 | new Spiral(p.color("#ff0000"), 0, this.startAngle + Math.PI / 2), 25 | new Spiral(p.color("#00ffcc"), 0, this.startAngle + Math.PI / 2 * 3), 26 | ]; 27 | 28 | while (spirals.filter((s) => s.radius !== 0).length === spirals.length) { 29 | for (const spiral of spirals) { 30 | spiral.draw(p); 31 | spiral.update(); 32 | } 33 | } 34 | 35 | this.startAngle += p.PI / 64; 36 | if (this.startAngle >= p.TWO_PI) { 37 | this.startAngle = this.startAngle - p.TWO_PI; 38 | } 39 | } 40 | 41 | } 42 | 43 | class Spiral { 44 | constructor( 45 | public color: p5.Color, 46 | public y: number, 47 | public angle: number, 48 | public yOff: number = -5, 49 | public angleOff: number = Math.PI / 16, 50 | public radius: number = 220, 51 | public radiusDec: number = 2, 52 | ) { } 53 | 54 | public update() { 55 | this.angle += this.angleOff; 56 | this.y += this.yOff; 57 | this.radius -= this.radiusDec; 58 | 59 | const alpha = 128 + 128 * Math.sin(this.angle); 60 | this.color.setAlpha(Math.max(alpha, 0)); 61 | } 62 | 63 | public draw(p: p5) { 64 | p.fill(this.color); 65 | p.noStroke(); 66 | 67 | p.ellipse(Math.cos(this.angle) * this.radius, this.y, 3); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sketches/spiral-noise.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class SpiralNoise implements ISketch { 4 | public readonly name = "Spiral Noise"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = false; 9 | public readonly padding = 40; 10 | private t = 0; 11 | 12 | public reset(p: p5) { 13 | p.background(255); 14 | } 15 | 16 | public draw(p: p5) { 17 | p.translate(this.width / 2, this.height / 2); 18 | 19 | const nVertices = p.random(8, 20); 20 | const deltaRadius = p.random(8, 10); 21 | 22 | const baseShape = []; 23 | for (let i = 0; i < nVertices - 1; ++i) { 24 | const a = i / nVertices * p.TWO_PI; 25 | const l = p.noise(this.t, i); 26 | 27 | baseShape.push(p5.Vector.fromAngle(a, l)); 28 | } 29 | 30 | p.noFill(); 31 | p.beginShape(); 32 | 33 | let insideCanvas = true; 34 | let r = 0; 35 | while (insideCanvas) { 36 | for (const pos of baseShape) { 37 | const x = pos.x * r; 38 | const y = pos.y * r; 39 | 40 | if (Math.abs(x) >= this.width / 2 - this.padding 41 | || Math.abs(y) >= this.height / 2 - this.padding) { 42 | insideCanvas = false; 43 | break; 44 | } 45 | 46 | p.vertex(x, y); 47 | r += deltaRadius / baseShape.length; 48 | } 49 | } 50 | 51 | p.endShape(); 52 | 53 | this.t += 0.1; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /sketches/super-permutations.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class SuperPermutations implements ISketch { 4 | public readonly name = "Super Permutation of 5"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | 10 | private i = 0; 11 | private seen = new Set(); 12 | 13 | public reset(p: p5) { 14 | p.background(40, 40, 40); 15 | 16 | this.i = 0; 17 | this.seen = new Set(); 18 | 19 | p.frameRate(5); 20 | } 21 | 22 | public draw(p: p5) { 23 | while (true) { 24 | if (this.i >= SuperPermutation5.length) { 25 | return; 26 | } 27 | 28 | const n = ( 29 | SuperPermutation5[this.i + 0] * 10000 + 30 | SuperPermutation5[this.i + 1] * 1000 + 31 | SuperPermutation5[this.i + 2] * 100 + 32 | SuperPermutation5[this.i + 3] * 10 + 33 | SuperPermutation5[this.i + 4] 34 | ); 35 | 36 | if (this.seen.has(n)) { 37 | this.i += 1; 38 | continue; 39 | } 40 | 41 | this.seen.add(n); 42 | 43 | const g = ( 44 | SuperPermutation5[this.i + 1] * 100 + 45 | SuperPermutation5[this.i + 2] * 10 + 46 | SuperPermutation5[this.i + 3] 47 | ) % 256; 48 | 49 | p.fill(g, g, g, 50); 50 | p.noStroke(); 51 | 52 | const t = n / 54321; 53 | 54 | const x = this.width / 2 + p.random(-t * 200, t * 200); 55 | const y = this.height / 2 + p.random(-t * 200, t * 200); 56 | 57 | p.ellipse(x, y, this.width / 2 * t, this.height / 2 * t); 58 | 59 | this.i += 1; 60 | 61 | break; 62 | } 63 | } 64 | } 65 | 66 | const SuperPermutation5 = [ 67 | 1, 2, 3, 4, 5, 68 | 6, 1, 2, 3, 4, 69 | 5, 1, 6, 2, 3, 70 | 4, 5, 1, 2, 6, 71 | 3, 4, 5, 1, 2, 72 | 3, 6, 4, 5, 1, 73 | 3, 2, 6, 4, 5, 74 | 1, 3, 6, 2, 4, 75 | 5, 1, 3, 6, 4, 76 | 2, 5, 1, 3, 6, 77 | 4, 5, 2, 1, 3, 78 | 6, 4, 5, 1, 2, 79 | 3, 4, 6, 5, 1, 80 | 2, 3, 4, 1, 5, 81 | 6, 2, 3, 4, 1, 82 | 5, 2, 6, 3, 4, 83 | 1, 5, 2, 3, 6, 84 | 4, 1, 5, 2, 3, 85 | 4, 6, 1, 5, 2, 86 | 3, 4, 1, 6, 5, 87 | 2, 3, 4, 1, 2, 88 | 5, 6, 3, 4, 1, 89 | 2, 5, 3, 6, 4, 90 | 1, 2, 5, 3, 4, 91 | 6, 1, 2, 5, 3, 92 | 4, 1, 6, 2, 5, 93 | 3, 4, 1, 2, 6, 94 | 5, 3, 4, 1, 2, 95 | 3, 5, 6, 4, 1, 96 | 2, 3, 5, 4, 6, 97 | 1, 2, 3, 5, 4, 98 | 1, 6, 2, 3, 5, 99 | 4, 1, 2, 6, 3, 100 | 5, 4, 1, 2, 3, 101 | 6, 5, 4, 1, 3, 102 | 2, 6, 5, 4, 3, 103 | 1, 2, 6, 4, 5, 104 | 3, 1, 6, 2, 4, 105 | 3, 5, 1, 6, 2, 106 | 4, 3, 1, 5, 6, 107 | 2, 4, 3, 1, 6, 108 | 5, 2, 4, 3, 1, 109 | 6, 2, 5, 4, 3, 110 | 1, 6, 2, 4, 5, 111 | 3, 1, 6, 4, 2, 112 | 5, 3, 1, 4, 6, 113 | 2, 5, 3, 1, 4, 114 | 2, 6, 5, 3, 1, 115 | 4, 2, 5, 6, 3, 116 | 1, 4, 2, 5, 3, 117 | 6, 1, 4, 2, 5, 118 | 3, 1, 6, 4, 5, 119 | 2, 3, 1, 4, 6, 120 | 5, 2, 3, 1, 4, 121 | 5, 6, 2, 3, 1, 122 | 4, 5, 2, 6, 3, 123 | 1, 4, 5, 2, 3, 124 | 6, 1, 4, 5, 2, 125 | 3, 1, 6, 4, 5, 126 | 3, 2, 1, 6, 4, 127 | 5, 3, 1, 2, 6, 128 | 4, 3, 5, 1, 2, 129 | 6, 4, 3, 1, 5, 130 | 2, 6, 4, 3, 1, 131 | 2, 5, 6, 4, 3, 132 | 2, 1, 5, 6, 4, 133 | 2, 3, 1, 5, 4, 134 | 6, 2, 3, 1, 5, 135 | 4, 2, 6, 3, 1, 136 | 5, 4, 2, 3, 6, 137 | 1, 5, 4, 2, 3, 138 | 1, 6, 5, 4, 2, 139 | 3, 1, 5, 6, 4, 140 | 2, 1, 3, 5, 6, 141 | 4, 2, 1, 5, 3, 142 | 6, 2, 4, 1, 5, 143 | 3, 6, 2, 1, 4, 144 | 5, 3, 6, 2, 1, 145 | 5, 4, 3, 6, 2, 146 | 1, 5, 3, 4, 6, 147 | 2, 1, 3, 5, 4, 148 | 6, 2, 1, 3, 4, 149 | 5, 6, 2, 1, 3, 150 | 4, 6, 5, 2, 1, 151 | 3, 4, 6, 2, 5, 152 | 1, 3, 4, 6, 2, 153 | 1, 5, 3, 6, 4, 154 | 2, 1, 5, 6, 3, 155 | 4, 2, 1, 6, 5, 156 | 3, 4, 2, 1, 6, 157 | 3, 5, 4, 2, 1, 158 | 6, 3, 4, 5, 2, 159 | 1, 6, 3, 4, 2, 160 | 5, 1, 6, 3, 4, 161 | 2, 1, 5, 6, 4, 162 | 3, 2, 5, 1, 6, 163 | 4, 3, 2, 5, 6, 164 | 1, 4, 3, 2, 5, 165 | 6, 4, 1, 3, 2, 166 | 5, 6, 4, 3, 1, 167 | 2, 6, 5, 4, 3, 168 | 2, 1, 6, 5, 4, 169 | 3, 2, 6, 1, 5, 170 | 3, 4, 2, 6, 1, 171 | 3, 5, 4, 2, 6, 172 | 1, 3, 4, 5, 2, 173 | 6, 1, 3, 4, 2, 174 | 5, 6, 1, 3, 4, 175 | 2, 6, 5, 1, 3, 176 | 4, 2, 6, 1, 5, 177 | 3, 2, 4, 6, 5, 178 | 1, 3, 2, 4, 6, 179 | 5, 3, 1, 2, 4, 180 | 6, 3, 5, 1, 2, 181 | 4, 6, 3, 1, 5, 182 | 2, 4, 6, 3, 1, 183 | 2, 5, 4, 6, 3, 184 | 2, 1, 5, 4, 6, 185 | 3, 2, 5, 1, 4, 186 | 6, 3, 2, 5, 4, 187 | 1, 6, 3, 2, 5, 188 | 4, 6, 1, 3, 2, 189 | 5, 4, 6, 3, 1, 190 | 2, 4, 5, 6, 3, 191 | 2, 1, 4, 5, 6, 192 | 3, 2, 4, 1, 5, 193 | 6, 3, 2, 4, 5, 194 | 1, 6, 3, 2, 4, 195 | 5, 6, 1, 3, 2, 196 | 4, 5, 6, 3, 1, 197 | 2, 4, 6, 5, 3, 198 | 2, 1, 4, 6, 5, 199 | 3, 2, 4, 1, 6, 200 | 5, 3, 2, 4, 6, 201 | 1, 5, 3, 2, 6, 202 | 4, 1, 5, 3, 2, 203 | 6, 1, 4, 5, 3, 204 | 2, 6, 1, 5, 4, 205 | 3, 2, 6, 5, 1, 206 | 4, 3, 6, 2, 5, 207 | 1, 4, 3, 6, 5, 208 | 2, 1, 4, 3, 5, 209 | 6, 2, 1, 4, 3, 210 | 5, 2, 6, 1, 4, 211 | 3, 5, 2, 1, 6, 212 | 4, 3, 5, 2, 1, 213 | 4, 6, 3, 5, 2, 214 | 1, 4, 3, 6, 5, 215 | 1, 2, 4, 3, 6, 216 | 1, 5, 2, 4, 3, 217 | 6, 1, 2, 5, 4, 218 | 3, 6, 1, 2, 4, 219 | 5, 3, 6, 1, 2, 220 | 4, 3, 5, 6, 1, 221 | 2, 4, 3, 6, 5, 222 | 1, 4, 2, 3, 5, 223 | 6, 1, 4, 2, 3, 224 | 5, 1, 6, 4, 2, 225 | 3, 5, 1, 4, 6, 226 | 2, 3, 5, 1, 4, 227 | 2, 6, 3, 5, 1, 228 | 4, 2, 3, 6, 5, 229 | 1, 4, 3, 2, 6, 230 | 5, 4, 1, 3, 6, 231 | 2, 5, 4, 1, 3, 232 | 6, 5, 2, 4, 1, 233 | 3, 5, 6, 2, 4, 234 | 1, 3, 5, 2, 6, 235 | 4, 1, 3, 5, 2, 236 | 4, 6, 1, 3, 5, 237 | 2, 4, 1, 6, 3, 238 | 5, 2, 4, 1, 3, 239 | 6, 5, 4, 2, 1, 240 | 3, 6, 5, 4, 1, 241 | 2, 3, 242 | ]; 243 | -------------------------------------------------------------------------------- /sketches/symmetry.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class NoiseSymmetry implements ISketch { 4 | public readonly name = "Noise Symmetry"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = true; 9 | private t = 0; 10 | 11 | private curve: [number, number][] = []; 12 | 13 | public reset(p: p5) { 14 | p.frameRate(15); 15 | 16 | p.background("white"); 17 | p.stroke(0, 0, 0, 100); 18 | 19 | let x = this.width / 2; 20 | let ystep = this.height / 2 / p.random(3, 10); 21 | 22 | this.curve = []; 23 | for (let y = 0; y < this.height / 2; y += ystep) { 24 | this.curve.push([x, y]); 25 | const t = p.noise(x, this.t) - 0.5; 26 | 27 | x += t * (this.width / 4); 28 | 29 | ystep = ystep * 0.9; 30 | } 31 | 32 | for (let j = this.curve.length - 2; j >= 0; --j) { 33 | this.curve.push([this.curve[j][0], this.height - this.curve[j][1]]); 34 | } 35 | } 36 | 37 | public draw(p: p5) { 38 | p.noFill(); 39 | 40 | const delta = 30; 41 | 42 | const curve: [number, number][] = []; 43 | for (let i = 0; i < this.curve.length; ++i) { 44 | const a = p.noise(this.t, ...this.curve[i]) * p.TWO_PI; 45 | 46 | if (p.random() < 0.1) { 47 | continue; 48 | } 49 | 50 | curve.push([ 51 | this.curve[i][0] + Math.cos(a) * delta, 52 | this.curve[i][1] + Math.sin(a) * delta, 53 | ]); 54 | } 55 | 56 | p.beginShape(); 57 | for (const [x, y] of curve) { 58 | p.curveVertex(x, y); 59 | } 60 | p.endShape(); 61 | 62 | p.beginShape(); 63 | for (const [x, y] of curve) { 64 | p.curveVertex(this.width - x, y); 65 | } 66 | p.endShape(); 67 | 68 | this.t += 0.1; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sketches/triangular-maze.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | type Triangle = [p5.Vector, p5.Vector, p5.Vector]; 4 | 5 | export class TriangularMaze implements ISketch { 6 | public readonly name = "Triangular Maze (kind of)"; 7 | 8 | public readonly width = 1920; 9 | public readonly height = 1080; 10 | public readonly loop = true; 11 | 12 | private seenCenters: p5.Vector[] = []; 13 | private stack: Triangle[] = []; 14 | 15 | public reset(p: p5) { 16 | p.frameRate(1); 17 | 18 | p.colorMode(p.HSB); 19 | const h1 = p.random(0, 360); 20 | p.background(h1, 80, 80); 21 | p.stroke((h1 + 180) % 360, 80, 80); 22 | 23 | this.seenCenters = []; 24 | 25 | const l = Math.min(this.width, this.height) / 2; 26 | this.stack = [ 27 | [ 28 | p.createVector(0, -Math.sqrt(3) / 2).mult(l), 29 | p.createVector(0.5, 0).mult(l), 30 | p.createVector(-0.5, 0).mult(l), 31 | ], 32 | ]; 33 | } 34 | 35 | public draw(p: p5) { 36 | p.push(); 37 | p.translate(this.width / 2, this.height / 2); 38 | 39 | p.noFill(); 40 | p.strokeWeight(5); 41 | 42 | const newStack: Triangle[] = []; 43 | 44 | for (const tri of this.stack) { 45 | const c = triangleCenter(tri); 46 | if (this.seenCenters.findIndex(sc => sc.dist(c) < 1e-6) >= 0) { 47 | continue; 48 | } 49 | this.seenCenters.push(c); 50 | 51 | this.drawTriangle(p, tri); 52 | 53 | for (const n of this.triangleNeighbors(p, tri)) { 54 | const nc = triangleCenter(n); 55 | if (nc.magSq() > Math.pow(this.width / 2, 2) + Math.pow(this.height / 2, 2)) { 56 | continue; 57 | } 58 | 59 | newStack.push(n); 60 | } 61 | } 62 | 63 | this.stack = newStack; 64 | p.pop(); 65 | } 66 | 67 | private drawTriangle(p: p5, tri: Triangle) { 68 | p.line(tri[0].x, tri[0].y, tri[1].x, tri[1].y); 69 | p.line(tri[1].x, tri[1].y, tri[2].x, tri[2].y); 70 | p.line(tri[2].x, tri[2].y, tri[0].x, tri[0].y); 71 | 72 | const steps = Math.floor(p.random(3, 20)); 73 | for (let e = 0; e < tri.length; ++e) { 74 | const a = tri[e]; 75 | const b = tri[(e + 1) % tri.length]; 76 | const c = tri[(e + 2) % tri.length]; 77 | 78 | for (let i = 0; i < steps; ++i) { 79 | const t = i / (steps - 1); 80 | 81 | const p1 = p5.Vector.lerp(a, b, t); 82 | const p2 = p5.Vector.lerp(a, c, t); 83 | for (let j = 0; j < i; ++j) { 84 | if (p.random() > 0.3) { 85 | continue; 86 | } 87 | 88 | const sp1 = p5.Vector.lerp(p1, p2, j / i); 89 | const sp2 = p5.Vector.lerp(p1, p2, (j + 1) / i); 90 | p.line(sp1.x, sp1.y, sp2.x, sp2.y); 91 | } 92 | } 93 | } 94 | } 95 | 96 | private triangleNeighbors(p: p5, tri: Triangle): Triangle[] { 97 | const third = (a: p5.Vector, b: p5.Vector): p5.Vector => { 98 | const mid = a 99 | .copy() 100 | .add(b) 101 | .div(2); 102 | const ba = b 103 | .copy() 104 | .sub(a) 105 | .normalize(); 106 | const off = p.createVector(ba.y, -ba.x).mult((a.dist(b) * Math.sqrt(3)) / 2); 107 | return mid.add(off); 108 | }; 109 | 110 | return [ 111 | [tri[1], tri[0], third(tri[0], tri[1])], 112 | [tri[2], tri[1], third(tri[1], tri[2])], 113 | [tri[0], tri[2], third(tri[2], tri[0])], 114 | ]; 115 | } 116 | } 117 | 118 | function triangleCenter(tri: Triangle): p5.Vector { 119 | return tri[0] 120 | .copy() 121 | .add(tri[1]) 122 | .add(tri[2]) 123 | .div(3); 124 | } 125 | -------------------------------------------------------------------------------- /sketches/truchet-tiles.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | type Tile = "triangle" | "quad-circles"; 4 | 5 | export class TruchetTiles implements ISketch { 6 | public readonly name = "Truchet Tiles"; 7 | 8 | public readonly width = 1920; 9 | public readonly height = 1080; 10 | public readonly loop = false; 11 | 12 | public readonly cellSize = 40; 13 | private tileType: Tile = "triangle"; 14 | 15 | public reset(p: p5) { 16 | if (p.random() > 0.5) { 17 | this.tileType = "triangle"; 18 | } else { 19 | this.tileType = "quad-circles"; 20 | } 21 | } 22 | 23 | public draw(p: p5) { 24 | p.colorMode(p.HSB); 25 | 26 | const h1 = p.random(0, 360); 27 | p.background(h1, 80, 80); 28 | 29 | const h2 = (h1 + 180) % 360; 30 | p.fill(h2, 80, 80); 31 | p.stroke(h2, 80, 80); 32 | 33 | switch (this.tileType) { 34 | case "triangle": 35 | this.drawTriangleTiles(p); 36 | break; 37 | case "quad-circles": 38 | this.drawQuadCircles(p); 39 | break; 40 | } 41 | } 42 | 43 | private drawTriangleTiles(p: p5) { 44 | p.noStroke(); 45 | 46 | for (let y = 0; y < this.height; y += this.cellSize) { 47 | for (let x = 0; x < this.width; x += this.cellSize) { 48 | const t = p.random(); 49 | 50 | if (t < 0.25) { 51 | p.triangle(x, y, x + this.cellSize, y, x, y + this.cellSize); 52 | } else if (t < 0.5) { 53 | p.triangle(x, y, x + this.cellSize, y, x + this.cellSize, y + this.cellSize); 54 | } else if (t < 0.75) { 55 | p.triangle(x + this.cellSize, y, x + this.cellSize, y + this.cellSize, x, y + this.cellSize); 56 | } else { 57 | p.triangle(x + this.cellSize, y + this.cellSize, x, y + this.cellSize, x, y); 58 | } 59 | } 60 | } 61 | } 62 | 63 | private drawQuadCircles(p: p5) { 64 | p.noFill(); 65 | p.strokeWeight(10); 66 | 67 | for (let y = 0; y < this.height; y += this.cellSize) { 68 | for (let x = 0; x < this.width; x += this.cellSize) { 69 | const t = p.random(); 70 | 71 | if (t > 0.5) { 72 | p.arc(x, y, this.cellSize, this.cellSize, 0, p.HALF_PI); 73 | p.arc(x + this.cellSize, y + this.cellSize, this.cellSize, this.cellSize, p.PI, -p.HALF_PI); 74 | } else { 75 | p.arc(x + this.cellSize, y, this.cellSize, this.cellSize, p.HALF_PI, p.PI); 76 | p.arc(x, y + this.cellSize, this.cellSize, this.cellSize, -p.HALF_PI, p.TWO_PI); 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sketches/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Pickup a random point in circle with the given radius. The center is assumed 4 | * to be centered in the origin. 5 | */ 6 | export function randomPointInCircle(p: p5, maxr: number): [number, number] { 7 | const a = p.random(0, p.TWO_PI); 8 | const r = p.random(1, maxr); 9 | 10 | return [Math.cos(a) * r, Math.sin(a) * r]; 11 | } 12 | 13 | /** 14 | * Sample the circle with the given radius at angles with a given offset between 15 | * each other. 16 | * @param tstep step angle in radians to sample the circle at 17 | * @param radius radius of the circle 18 | */ 19 | export function* sampleCircle(tstep: number, radius: number): Iterable<[number, number]> { 20 | for (let t = 0; t <= Math.PI * 2; t += tstep) { 21 | const x = radius * Math.cos(t); 22 | const y = radius * Math.sin(t); 23 | 24 | yield [x, y]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sketches/vornoi.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class Voronoi implements ISketch { 4 | public readonly name = "Voronoi"; 5 | 6 | public readonly width = 1920; 7 | public readonly height = 1080; 8 | public readonly loop = false; 9 | 10 | private pivots: { pos: p5.Vector; color: p5.Color }[] = []; 11 | 12 | public reset(p: p5) { 13 | this.pivots = []; 14 | 15 | const n = p.random(10, 50); 16 | for (let i = 0; i < n; ++i) { 17 | const pv = p.createVector(p.random(this.width), p.random(this.height)); 18 | 19 | this.pivots.push({ 20 | pos: pv, 21 | color: p.color( 22 | p.random([ 23 | "#5f8dd3", 24 | "#438e90", 25 | "#4f643a", 26 | "#7b8db9", 27 | "#64a943", 28 | "#3ca64b", 29 | "#99b8c9", 30 | "#83c066", 31 | "#ebbe75", 32 | "#c44da2", 33 | "#937218", 34 | "#9c531e", 35 | "#435596", 36 | "#9a27cc", 37 | "#d04143", 38 | "#387389", 39 | "#5d48d4", 40 | "#6faea2", 41 | "#d89d4b", 42 | "#337fb9", 43 | ]), 44 | ), 45 | }); 46 | } 47 | } 48 | 49 | public draw(p: p5) { 50 | p.background("white"); 51 | 52 | p.loadPixels(); 53 | 54 | for (let y = 0; y < this.height; ++y) { 55 | for (let x = 0; x < this.width; ++x) { 56 | let minD = Infinity; 57 | let col: p5.Color; 58 | for (const pivot of this.pivots) { 59 | const d = Math.abs(pivot.pos.x - x) + Math.abs(pivot.pos.y - y); 60 | if (d < minD) { 61 | minD = d; 62 | col = pivot.color; 63 | } 64 | } 65 | 66 | p.set(x, y, col!); 67 | } 68 | } 69 | 70 | p.updatePixels(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /sketches/walls.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from "./sketch"; 2 | 3 | export class Walls implements ISketch { 4 | public readonly name = "Walls"; 5 | 6 | public readonly width = 1300; 7 | public readonly height = 800; 8 | public readonly loop = false; 9 | 10 | public readonly generations = 50; 11 | 12 | private time = 0; 13 | 14 | public reset(p: p5) { 15 | p.background(80, 80, 80); 16 | 17 | this.time += 1; 18 | } 19 | 20 | public draw(p: p5) { 21 | p.noFill(); 22 | p.stroke(255, 255, 255, 100); 23 | 24 | p.translate(0, this.height / 2); 25 | this.drawPlanesStrip(p, 5); 26 | 27 | p.translate(0, this.height / 2); 28 | this.drawPlanesStrip(p, 7); 29 | } 30 | 31 | public drawPlanesStrip(p: p5, id: number) { 32 | const points: p5.Vector[] = []; 33 | 34 | for (let i = 0; i < 20; ++i) { 35 | const a = -p.map(i, 0, 19, p.HALF_PI, p.PI); 36 | points.push( 37 | p.createVector( 38 | Math.cos(a) * 300 + p.random(-10, 10), 39 | Math.sin(a) * 300 + p.random(-10, 10), 40 | ), 41 | ); 42 | } 43 | 44 | for (let pi = 0; pi < points.length - 1; ++pi) { 45 | p.ellipse(points[pi].x, points[pi].y, 5); 46 | p.line(points[pi].x, points[pi].y, points[pi + 1].x, points[pi + 1].y); 47 | } 48 | 49 | for (let genId = 0; genId < this.generations; ++genId) { 50 | const a = p.map(p.noise(this.time, id, genId), 0, 1, -p.HALF_PI / 2, p.HALF_PI / 2); 51 | 52 | for (const pt of points) { 53 | const dx = Math.cos(a) * 50; 54 | const dy = Math.sin(a) * 30; 55 | 56 | p.line(pt.x, pt.y, pt.x + dx, pt.y + dy); 57 | 58 | pt.add(dx, dy); 59 | } 60 | 61 | for (let pi = 0; pi < points.length - 1; ++pi) { 62 | p.ellipse(points[pi].x, points[pi].y, 5); 63 | p.line(points[pi].x, points[pi].y, points[pi + 1].x, points[pi + 1].y); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // 4 | // base 5 | // 6 | "target": "es6", 7 | "lib": [ 8 | "es5", 9 | "es6", 10 | "dom" 11 | ], 12 | 13 | // we need the moduleResolution be be node because of 14 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/24788. 15 | "moduleResolution": "node", 16 | 17 | // 18 | // UI 19 | // 20 | "jsx": "react", 21 | 22 | // 23 | // strictness 24 | // 25 | "alwaysStrict": true, 26 | "strict": true, 27 | "strictFunctionTypes": true, 28 | "strictPropertyInitialization": true, 29 | "strictNullChecks": true, 30 | 31 | // misc 32 | "sourceMap": true, 33 | 34 | "removeComments": false, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "max-classes-per-file": false, 9 | "no-console": false, 10 | }, 11 | "rulesDirectory": [] 12 | } 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | const dist = path.resolve(__dirname, "dist"); 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 6 | 7 | module.exports = { 8 | entry: "./index.tsx", 9 | output: { 10 | path: dist, 11 | filename: "bundle.js", 12 | chunkFilename: '[name].bundle.js', 13 | }, 14 | 15 | devtool: "source-map", 16 | 17 | devServer: { 18 | contentBase: dist, 19 | }, 20 | 21 | plugins: [ 22 | new HtmlWebpackPlugin({ 23 | template: 'index.html' 24 | }), 25 | 26 | new MiniCssExtractPlugin({ 27 | filename: "[name].css", 28 | chunkFilename: "[id].css" 29 | }), 30 | ], 31 | 32 | resolve: { 33 | extensions: [".ts", ".tsx", ".js", ".scss"] 34 | }, 35 | 36 | module: { 37 | rules: [ 38 | 39 | { 40 | test: /\.tsx?$/, 41 | loader: "awesome-typescript-loader" 42 | }, 43 | 44 | { 45 | enforce: "pre", 46 | test: /\.js$/, 47 | loader: "source-map-loader" 48 | }, 49 | 50 | { 51 | test: /\.scss$/, 52 | use: [ 53 | process.env.NODE_ENV !== 'production' ? 'style-loader' : MiniCssExtractPlugin.loader, 54 | "css-loader", 55 | "postcss-loader", // spectre.css needs autoprefixer 56 | "sass-loader" 57 | ] 58 | } 59 | ] 60 | }, 61 | }; 62 | --------------------------------------------------------------------------------