├── docs ├── _config.yml ├── .gitignore ├── identicon.ico ├── assets │ └── css │ │ ├── style.scss │ │ └── additions.css ├── 404.html ├── crossfade-maker.py ├── Gemfile └── index.md ├── .gitignore ├── setup ├── set-wallpaper-1.3 ├── set-wallpaper-1.2 ├── set-wallpaper-1.0 └── wallrnd.toml ├── prototype ├── set-wallpaper └── prototype.py ├── src ├── salt.rs ├── frame.rs ├── lib.rs ├── shape.rs ├── color.rs ├── chooser.rs ├── pos.rs ├── svg.rs ├── cfg.rs ├── paint.rs ├── scene.rs ├── main.rs ├── log.rs ├── tesselate.rs └── deserializer.rs ├── Cargo.toml ├── assets ├── man └── default.toml └── README.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /docs/identicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vanille-N/wallrnd/HEAD/docs/identicon.ico -------------------------------------------------------------------------------- /docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | @import "additions.css" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.svg 3 | *.png 4 | Cargo.lock 5 | !docs/samples/* 6 | TODO 7 | .sass-cache 8 | *.txt 9 | *.jpg 10 | *.xcf 11 | *.pdf 12 | test.toml 13 | -------------------------------------------------------------------------------- /setup/set-wallpaper-1.3: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Set the output image of `wallrnd` as wallpaper 4 | # Tested on Ubuntu 20.04 (Focal Fossa) 5 | 6 | # Disable when running on battery 7 | ac_adapter=$(acpi -a | cut -d' ' -f3 | cut -d- -f1) 8 | 9 | if [ "$ac_adapter" = "off" ]; then 10 | exit 100 11 | fi 12 | 13 | IMG=/tmp/wallpaper.svg 14 | CFG=/home/$( whoami )/wallrnd.toml 15 | wallrnd --image=$IMG --config=$CFG --set --nice 16 | -------------------------------------------------------------------------------- /setup/set-wallpaper-1.2: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Set the output image of `wallrnd` as wallpaper 4 | # Tested on Ubuntu 20.04 (Focal Fossa) 5 | 6 | # Disable when running on battery 7 | ac_adapter=$(acpi -a | cut -d' ' -f3 | cut -d- -f1) 8 | 9 | if [ "$ac_adapter" = "off" ]; then 10 | exit 100 11 | fi 12 | 13 | IMG=/tmp/wallpaper.svg 14 | CFG=/home/$( whoami )/wallrnd.toml 15 | nice -n 20 -- wallrnd --image=$IMG --config=$CFG --set 16 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | 18 | 19 |
20 |

404

21 | 22 |

Page not found :(

23 |

The requested page could not be found.

24 |
25 | -------------------------------------------------------------------------------- /docs/crossfade-maker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | NB_FIG = 6 4 | TIME = 5 5 | 6 | for i in range(NB_FIG): 7 | print("""figure:nth-child({}) {{ 8 | animation: xfade {}s {}s infinite; 9 | }}""".format(i+1, TIME * NB_FIG, TIME * (NB_FIG - i - 1))) 10 | 11 | print(""" 12 | 13 | @keyframes xfade {{ 14 | 0% {{ 15 | opacity: 1; 16 | }} 17 | {}% {{ 18 | opacity: 1; 19 | }} 20 | {}% {{ 21 | opacity: 0; 22 | }} 23 | 98% {{ 24 | opacity: 0; 25 | }} 26 | 100% {{ 27 | opacity: 1; 28 | }} 29 | }} 30 | """.format(100/NB_FIG, 100/NB_FIG*1.5)) 31 | -------------------------------------------------------------------------------- /prototype/set-wallpaper: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Set the output image of `prototype.py` as wallpaper 4 | # Tested on Ubuntu 20.04 (Focal Fossa) 5 | 6 | user=$(whoami) 7 | 8 | fl=$(find /proc -maxdepth 2 -user $user -name environ -print -quit) 9 | while [ -z $(grep -z DBUS_SESSION_BUS_ADDRESS "$fl" | cut -d= -f2- | tr -d '\000' ) ] 10 | do 11 | fl=$(find /proc -maxdepth 2 -user $user -name environ -newer "$fl" -print -quit) 12 | done 13 | 14 | export DBUS_SESSION_BUS_ADDRESS=$(grep -z DBUS_SESSION_BUS_ADDRESS "$fl" | cut -d= -f2-) 15 | 16 | nice -n 20 -- ~/Algo/Repos/wallrnd/prototype.py 17 | IMG="/tmp/wallpaper-random.svg" 18 | 19 | nice -n 20 -- dconf write "/org/gnome/desktop/background/picture-uri" "'file://${IMG}'" 20 | -------------------------------------------------------------------------------- /src/salt.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use rand::{rngs::ThreadRng, Rng}; 3 | 4 | #[derive(Clone, Copy, Debug)] 5 | pub struct SaltItem { 6 | pub color: Color, 7 | pub likeliness: f64, 8 | pub variability: usize, 9 | } 10 | 11 | #[derive(Clone, Debug, Default)] 12 | pub struct Salt(pub Vec); 13 | 14 | impl SaltItem { 15 | fn sample(&self, rng: &mut ThreadRng) -> Option { 16 | if rng.gen::() < self.likeliness { 17 | Some(self.color.variate(rng, self.variability)) 18 | } else { 19 | None 20 | } 21 | } 22 | } 23 | 24 | impl Salt { 25 | pub fn sample(&self, rng: &mut ThreadRng) -> Option { 26 | for item in self.0.iter() { 27 | if let Some(c) = item.sample(rng) { 28 | return Some(c); 29 | } 30 | } 31 | None 32 | } 33 | 34 | pub fn none() -> Self { 35 | Self(Vec::new()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /setup/set-wallpaper-1.0: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Set the output image of `wallrnd` as wallpaper 4 | # Tested on Ubuntu 20.04 (Focal Fossa) 5 | 6 | # Disable when running on battery 7 | ac_adapter=$(acpi -a | cut -d' ' -f3 | cut -d- -f1) 8 | 9 | if [ "$ac_adapter" = "off" ]; then 10 | exit 100 11 | fi 12 | 13 | # Setup environment to allow the cronjob to change wallpaper 14 | user=$(whoami) 15 | 16 | fl=$(find /proc -maxdepth 2 -user $user -name environ -print -quit) 17 | while [ -z $(grep -z DBUS_SESSION_BUS_ADDRESS "$fl" | cut -d= -f2- | tr -d '\000' ) ] 18 | do 19 | fl=$(find /proc -maxdepth 2 -user $user -name environ -newer "$fl" -print -quit) 20 | done 21 | 22 | export DBUS_SESSION_BUS_ADDRESS=$(grep -z DBUS_SESSION_BUS_ADDRESS "$fl" | cut -d= -f2-) 23 | 24 | # Generate image 25 | DIR="/path/to/executable" 26 | IMG="/tmp/wallpaper-random.svg" 27 | nice -n 20 -- $DIR/wallrnd --image $IMG --config $DIR/wallrnd.toml 28 | 29 | # Set as wallpaper 30 | nice -n 20 -- dconf write "/org/gnome/desktop/background/picture-uri" "'file://${IMG}'" 31 | -------------------------------------------------------------------------------- /src/frame.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct Frame { 5 | pub x: usize, 6 | pub y: usize, 7 | pub w: usize, 8 | pub h: usize, 9 | } 10 | 11 | impl Frame { 12 | /// Extract dimensions to SVG-compatible format 13 | pub fn into_tuple(self) -> (usize, usize, usize, usize) { 14 | (self.x, self.y, self.x + self.w, self.y + self.h) 15 | } 16 | 17 | /// Midpoint of frame 18 | pub fn center(&self) -> Pos { 19 | Pos((self.x + self.w / 2) as f64, (self.y + self.h / 2) as f64) 20 | } 21 | 22 | /// Check that point is within some distance of the frame (include points that are not far outside) 23 | pub fn is_inside(&self, pos: Pos) -> bool { 24 | let xerr = (self.w as f64) / 10.; 25 | let yerr = (self.h as f64) / 10.; 26 | (self.x as f64 - xerr) < pos.0 27 | && pos.0 < (self.x + self.w) as f64 + xerr 28 | && (self.y as f64 - yerr) < pos.1 29 | && pos.1 < (self.y + self.h) as f64 + yerr 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wallrnd" 3 | version = "1.2.2" 4 | authors = ["Vanille-N "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "A generator of random abstract wallpapers" 8 | readme = "README.md" 9 | repository = "https://github.com/Vanille-N/wallrnd" 10 | keywords = ["wallpaper", "random", "desktop"] 11 | categories = ["command-line-utilities", "graphics"] 12 | include = ["src/**/*", "README.md", "assets/*"] 13 | 14 | [features] 15 | default = [] 16 | all = ["set-wallpaper", "make-png", "nice"] 17 | set-wallpaper = ["wallpaper_rs"] 18 | make-png = ["resvg", "usvg"] 19 | nice = ["scrummage"] 20 | 21 | [dependencies] 22 | rand = "0.7.*" 23 | serde = "1.0.*" 24 | serde_derive = "1.0.*" 25 | toml = "0.5.*" 26 | chrono = "0.4.*" 27 | delaunator = "0.2.*" 28 | resvg = { version = "0.11.*", optional = true } # MPL 2.0 29 | usvg = { version = "0.11.*", optional = true } # MPL 2.0 30 | wallpaper_rs = { version = "0.1.0", optional = true } # GPL 3.0 31 | scrummage = { version = "0.1.1", optional = true } 32 | 33 | [[bin]] 34 | name = "wallrnd" 35 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Hello! This is where you manage which Jekyll version is used to run. 4 | # When you want to use a different version, change it below, save the 5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 6 | # 7 | # bundle exec jekyll serve 8 | # 9 | # This will help ensure the proper Jekyll version is running. 10 | # Happy Jekylling! 11 | gem "jekyll", "~> 3.8.7" 12 | 13 | gem "kramdown", ">= 2.3.0" 14 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 15 | # gem "minima", "~> 2.0" 16 | 17 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 18 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 19 | gem "github-pages", group: :jekyll_plugins 20 | 21 | # If you have any plugins, put them here! 22 | group :jekyll_plugins do 23 | gem "jekyll-feed", "~> 0.6" 24 | end 25 | 26 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 27 | # and associated library. 28 | install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do 29 | gem "tzinfo", "~> 1.2" 30 | gem "tzinfo-data" 31 | end 32 | 33 | # Performance-booster for watching directories on Windows 34 | gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform? 35 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cfg; 2 | pub mod chooser; 3 | pub mod color; 4 | pub mod deserializer; 5 | pub mod frame; 6 | pub mod log; 7 | pub mod paint; 8 | pub mod pos; 9 | pub mod salt; 10 | pub mod scene; 11 | pub mod shape; 12 | pub mod svg; 13 | pub mod tesselate; 14 | 15 | pub mod prelude { 16 | pub use super::Verbosity; 17 | use super::*; 18 | pub use cfg::{Pattern, Tiling}; 19 | pub use chooser::Chooser; 20 | pub use color::Color; 21 | pub use frame::Frame; 22 | pub use pos::{radians, Pos}; 23 | pub use salt::{Salt, SaltItem}; 24 | 25 | use std::collections::HashMap; 26 | pub type ColorList = HashMap; 27 | pub type ThemeList = HashMap>; 28 | 29 | #[derive(Clone, Debug)] 30 | pub struct ThemeItem(pub Color, pub Option, pub Option, pub Salt); 31 | } 32 | 33 | #[derive(Clone, Copy, Default)] 34 | pub struct Verbosity { 35 | pub info: bool, 36 | pub warn: bool, 37 | pub prog: bool, 38 | pub details: bool, 39 | } 40 | 41 | impl Verbosity { 42 | pub fn from(s: &str) -> Self { 43 | let mut v = Verbosity::default(); 44 | for option in s.chars() { 45 | match option { 46 | 'A' => { 47 | v.info = true; 48 | v.warn = true; 49 | v.prog = true; 50 | v.details = true; 51 | } 52 | 'I' => v.info = true, 53 | 'W' => v.warn = true, 54 | 'P' => v.prog = true, 55 | 'D' => v.details = true, 56 | c => println!( 57 | "Unknown verbosity option '{}', use one or more of 'IWPDA'", 58 | c 59 | ), 60 | } 61 | } 62 | v 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/shape.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::svg::*; 3 | 4 | /// A generic shape that can be placed at any position according to a given center 5 | pub struct Movable(Vec); 6 | 7 | impl Movable { 8 | pub fn render(&self, reference: Pos) -> (Pos, Path) { 9 | let mut data = Data::new(reference + self.0[0]); 10 | for p in self.0.iter().skip(1) { 11 | data.line_to(reference + *p); 12 | } 13 | (reference, Path::new(data)) 14 | } 15 | 16 | pub fn hexagon(size: f64, rot: isize) -> Self { 17 | let mut pts = Vec::new(); 18 | for i in 0..6 { 19 | pts.push(Pos::polar(rot + 60 * i, size)) 20 | } 21 | Movable(pts) 22 | } 23 | 24 | pub fn triangle(size: f64, rot: isize) -> Self { 25 | let mut pts = Vec::new(); 26 | for i in 0..3 { 27 | pts.push(Pos::polar(rot + 120 * i, size)) 28 | } 29 | Movable(pts) 30 | } 31 | 32 | pub fn square(size: f64, rot: isize) -> Self { 33 | let mut pts = Vec::new(); 34 | for i in 0..4 { 35 | pts.push(Pos::polar(rot + 45 + 90 * i, size)) 36 | } 37 | Movable(pts) 38 | } 39 | 40 | pub fn rhombus(ldiag: f64, sdiag: f64, rot: isize) -> Self { 41 | Movable(vec![ 42 | Pos::polar(rot, ldiag), 43 | Pos::polar(rot + 90, sdiag), 44 | Pos::polar(rot + 180, ldiag), 45 | Pos::polar(rot + 270, sdiag), 46 | ]) 47 | } 48 | 49 | pub fn from(v: Vec) -> Self { 50 | Self(v) 51 | } 52 | 53 | pub fn vertex(&self, idx: usize) -> Pos { 54 | self.0[idx % self.0.len()] 55 | } 56 | 57 | pub fn side(&self, idx: usize) -> Pos { 58 | self.0[(idx + 1) % self.0.len()] - self.0[idx % self.0.len()] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /assets/man: -------------------------------------------------------------------------------- 1 | WALLRND 2 | 3 | NAME 4 | wallrnd 5 | github.com/Vanille-N/wallrnd 6 | crates.io/crates/wallrnd 7 | 8 | SYNOPSIS 9 | wallrnd [OPTIONS] 10 | 11 | DESCRIPTION 12 | wallrnd - A highly configurable generator of abstract random wallpapers 13 | 14 | OPTIONS 15 | --help Print this help and exit 16 | --log F Save generation information for image replication in file F 17 | --verbose V Display more debug information. See verbosity description below. 18 | --time T Generate image as if the current time was T (format HHMM) 19 | --image I Destination of the generated file. If absent or invalid, program aborts. Not necessarily absolute path. 20 | --config C Location of the config file. If absent or invalid, default parameters are used. 21 | --init C Create a default configuration in file C and exit. 22 | --set Set as wallpaper (requires image to be saved). Absolute path recommended for --image. 23 | --nice Lower process priority to run in the background (recommended) 24 | 25 | EXAMPLES 26 | wallrnd --image /tmp/random-wallpaper.svg --config ~/.config/wallrnd.toml --set --nice 27 | wallrnd --verbose IP --log save.txt --time 1000 --image test.svg 28 | wallrnd --init default.toml 29 | 30 | VERBOSITY 31 | By default, wallrnd is silent. 32 | It is possible to adjust verbosity level with flags. 33 | 34 | 'I': Info Display basic information (theme, shapes, ...) 35 | 'P': Progress Inform on the current actions 36 | 'D': Details Very verbose, prints full scene layout and list of colors 37 | 'W': Warnings Alert recoverable errors (invalid configuration file, badly formatted color, ...) 38 | 39 | Any combination of these flags is valid except for no flags 40 | ('--verbose' option expects at least one flag) 41 | 42 | 'A' (All) is an alias for 'IPDW' 43 | -------------------------------------------------------------------------------- /docs/assets/css/additions.css: -------------------------------------------------------------------------------- 1 | /* Credits for the crossfade to https://gist.github.com/A973C/7068921*/ 2 | body{ 3 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 4 | font-weight: 300; 5 | } 6 | .css-slideshow{ 7 | position: relative; 8 | max-width: 850px; 9 | height: 500px; 10 | margin: 5em auto .5em auto; 11 | } 12 | .css-slideshow figure{ 13 | margin: 0; 14 | max-width: 850px; 15 | height: 500px; 16 | background: #000; 17 | position: absolute; 18 | } 19 | .css-slideshow img{ 20 | box-shadow: 0 0 2px #666; 21 | } 22 | .css-slideshow figcaption{ 23 | position: absolute; 24 | top: 0; 25 | color: #fff; 26 | background: rgba(0,0,0, .8); 27 | font-size: 1.5em; 28 | padding: 8px 12px; 29 | opacity: 0; 30 | transition: opacity .5s; 31 | } 32 | .css-slideshow:hover figure figcaption{ 33 | transition: opacity .5s; 34 | opacity: 1; 35 | } 36 | .css-slideshow-attr{ 37 | max-width: 495px; 38 | text-align: right; 39 | font-size: .7em; 40 | font-style: italic; 41 | margin:0 auto; 42 | } 43 | .css-slideshow-attr a{ 44 | color: #666; 45 | } 46 | .css-slideshow figure{ 47 | opacity:0; 48 | } 49 | 50 | /* >>>>>>>>>> BEGIN CROSSFADE GENERATION */ 51 | figure:nth-child(1) { 52 | animation: xfade 30s 25s infinite; 53 | } 54 | figure:nth-child(2) { 55 | animation: xfade 30s 20s infinite; 56 | } 57 | figure:nth-child(3) { 58 | animation: xfade 30s 15s infinite; 59 | } 60 | figure:nth-child(4) { 61 | animation: xfade 30s 10s infinite; 62 | } 63 | figure:nth-child(5) { 64 | animation: xfade 30s 5s infinite; 65 | } 66 | figure:nth-child(6) { 67 | animation: xfade 30s 0s infinite; 68 | } 69 | 70 | 71 | @keyframes xfade { 72 | 0%{ 73 | opacity: 1; 74 | } 75 | 16.666666666666668%{ 76 | opacity: 1; 77 | } 78 | 25.0%{ 79 | opacity: 0; 80 | } 81 | 98%{ 82 | opacity: 0; 83 | } 84 | 100%{ 85 | opacity: 1; 86 | } 87 | } 88 | 89 | /* >>>>>>>>>> END CROSSFADE GENERATION */ 90 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | use rand::{rngs::ThreadRng, Rng}; 2 | use std::convert::TryInto; 3 | use std::fmt; 4 | 5 | #[derive(Clone, Copy, Debug)] 6 | pub struct Color(pub usize, pub usize, pub usize); 7 | 8 | impl Color { 9 | /// Ensure that all RGB values are within [[1; 100]] 10 | fn validate(mut self) -> Self { 11 | self.0 = self.0.min(255); 12 | self.1 = self.1.min(255); 13 | self.2 = self.2.min(255); 14 | self 15 | } 16 | 17 | /// Random noise 18 | pub fn variate(mut self, rng: &mut ThreadRng, amount: usize) -> Self { 19 | if amount > 0 { 20 | let amount = amount as isize; 21 | self.0 = (self.0 as isize + rng.gen_range(-amount as isize, amount as isize)) 22 | .try_into() 23 | .unwrap_or(0); 24 | self.1 = (self.1 as isize + rng.gen_range(-amount as isize, amount as isize)) 25 | .try_into() 26 | .unwrap_or(0); 27 | self.2 = (self.2 as isize + rng.gen_range(-amount as isize, amount as isize)) 28 | .try_into() 29 | .unwrap_or(0); 30 | } 31 | self 32 | } 33 | 34 | /// Weighted mix with other color 35 | pub fn meanpoint(mut self, th: Self, distance: usize) -> Self { 36 | self.0 = (self.0 * distance + th.0 * (100 - distance)) / 100; 37 | self.1 = (self.1 * distance + th.1 * (100 - distance)) / 100; 38 | self.2 = (self.2 * distance + th.2 * (100 - distance)) / 100; 39 | self 40 | } 41 | 42 | /// Generate color 43 | pub fn random(rng: &mut ThreadRng) -> Self { 44 | Self( 45 | rng.gen_range(0, 255), 46 | rng.gen_range(0, 255), 47 | rng.gen_range(0, 255), 48 | ) 49 | } 50 | } 51 | 52 | /// SVG color format: `rgb(,,)` 53 | impl fmt::Display for Color { 54 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 55 | let c = self.validate(); 56 | write!(f, "rgb({},{},{})", c.0, c.1, c.2) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/chooser.rs: -------------------------------------------------------------------------------- 1 | use rand::{rngs::ThreadRng, Rng}; 2 | 3 | #[derive(Clone)] 4 | pub struct Chooser(usize, Vec<(T, usize)>); 5 | 6 | impl Chooser { 7 | /// Empty Chooser 8 | pub fn default() -> Self { 9 | Self(0, Vec::new()) 10 | } 11 | 12 | /// Create Chooser from weighted items 13 | pub fn new(mut v: Vec<(T, usize)>) -> Self { 14 | let mut sum = 0; 15 | for (_, w) in &mut v { 16 | sum += *w; 17 | *w = sum; 18 | } 19 | if !v.is_empty() { 20 | Self(sum, v) 21 | } else { 22 | Self::default() 23 | } 24 | } 25 | 26 | /// Pick a random item (weighted) 27 | pub fn choose(&self, rng: &mut ThreadRng) -> Option { 28 | if self.1.is_empty() { 29 | None 30 | } else { 31 | let choice = rng.gen_range(0, self.0); 32 | Some(self.dichotomy(choice, 0, self.1.len())) 33 | } 34 | } 35 | 36 | fn dichotomy(&self, target: usize, inf: usize, sup: usize) -> T { 37 | if inf == sup { 38 | self.1[inf].0.clone() 39 | } else if inf + 1 == sup { 40 | if self.1[inf].1 < target { 41 | self.1[inf + 1].0.clone() 42 | } else { 43 | self.1[inf].0.clone() 44 | } 45 | } else { 46 | let mid = (sup + inf) / 2; 47 | if self.1[mid].1 > target { 48 | self.dichotomy(target, inf, mid) 49 | } else { 50 | self.dichotomy(target, mid, sup) 51 | } 52 | } 53 | } 54 | 55 | /// Get items with their weights as a copy 56 | pub fn extract(&self) -> Vec<(T, usize)> { 57 | let mut cpy = self.1.clone(); 58 | let n = cpy.len(); 59 | for i in 1..n { 60 | cpy[n - i].1 -= cpy[n - i - 1].1; 61 | } 62 | cpy 63 | } 64 | 65 | /// Add new item 66 | pub fn push(&mut self, item: T, w: usize) { 67 | if w == 0 { 68 | panic!("In call to Chooser::push, 0 is not a valid weight"); 69 | } 70 | self.0 += w; 71 | self.1.push((item, self.0)); 72 | } 73 | 74 | /// Add vector of new items 75 | pub fn append(&mut self, items: Vec<(T, usize)>) { 76 | for (item, w) in items { 77 | self.push(item, w); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/pos.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use rand::{rngs::ThreadRng, Rng}; 3 | use std::cmp::{Eq, PartialEq}; 4 | use std::f64::consts::PI; 5 | use std::hash::{Hash, Hasher}; 6 | use std::ops::{Add, Mul, Neg, Sub}; 7 | 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Pos(pub f64, pub f64); 10 | 11 | impl Pos { 12 | pub fn into_tuple(self) -> (f64, f64) { 13 | (self.0, self.1) 14 | } 15 | 16 | // Nearest 100'th of unit 17 | pub fn round(self) -> (i32, i32) { 18 | ( 19 | (self.0 * 100.).round() as i32, 20 | (self.1 * 100.).round() as i32, 21 | ) 22 | } 23 | 24 | pub fn norm(self) -> f64 { 25 | self.dot_self().sqrt() 26 | } 27 | 28 | pub fn unit(self) -> Self { 29 | self * (1. / self.norm()) 30 | } 31 | 32 | pub fn dot_self(self) -> f64 { 33 | self.0.powi(2) + self.1.powi(2) 34 | } 35 | 36 | pub fn dot(self, other: Self) -> f64 { 37 | self.0 * other.0 + self.1 * other.1 38 | } 39 | 40 | pub fn random(f: &Frame, rng: &mut ThreadRng) -> Self { 41 | let errx = f.w as f64 / 10.; 42 | let erry = f.h as f64 / 10.; 43 | let x = f.x as f64 - errx + rng.gen::() * f.w as f64 * 1.2; 44 | let y = f.y as f64 - erry + rng.gen::() * f.h as f64 * 1.2; 45 | Self(x, y) 46 | } 47 | 48 | pub fn dist(self, other: Self) -> f64 { 49 | (self - other).norm() 50 | } 51 | 52 | pub fn project(self, other: Self) -> Self { 53 | let other = other.unit(); 54 | other * self.dot(other) 55 | } 56 | 57 | pub fn polar(a: isize, r: f64) -> Self { 58 | let theta = radians(a); 59 | Pos(r * theta.cos(), r * theta.sin()) 60 | } 61 | 62 | pub fn intersect((pos1, rot1): (Self, isize), (pos2, rot2): (Self, isize)) -> Self { 63 | let pos1b = pos1 + Pos::polar(rot1, 1.); 64 | let pos2b = pos2 + Pos::polar(rot2, 1.); 65 | 66 | let dx = Pos(pos1.0 - pos1b.0, pos2.0 - pos2b.0); 67 | let dy = Pos(pos1.1 - pos1b.1, pos2.1 - pos2b.1); 68 | 69 | let det = |a: Pos, b: Pos| a.0 * b.1 - a.1 * b.0; 70 | 71 | let inv = { 72 | let div = det(dx, dy); 73 | if div.abs() < 0.01 { 74 | panic!("Malformed intersection"); 75 | } 76 | 1. / div 77 | }; 78 | 79 | let d = Pos(det(pos1, pos1b), det(pos2, pos2b)); 80 | let x = det(d, dx) * inv; 81 | let y = det(d, dy) * inv; 82 | Pos(x, y) 83 | } 84 | 85 | pub fn zero() -> Self { 86 | Self(0., 0.) 87 | } 88 | } 89 | 90 | pub fn crossprod_sign(a: Pos, b: Pos, c: Pos) -> bool { 91 | (a.0 - c.0) * (b.1 - c.1) - (b.0 - c.0) * (a.1 - c.1) > 0. 92 | } 93 | 94 | pub fn radians(a: isize) -> f64 { 95 | (a as f64) * PI / 180. 96 | } 97 | 98 | impl Add<(f64, f64)> for Pos { 99 | type Output = Self; 100 | fn add(self, (x, y): (f64, f64)) -> Self::Output { 101 | Pos(self.0 + x, self.1 + y) 102 | } 103 | } 104 | 105 | impl Add for Pos { 106 | type Output = Self; 107 | fn add(self, Pos(x, y): Pos) -> Self::Output { 108 | Pos(self.0 + x, self.1 + y) 109 | } 110 | } 111 | 112 | impl Sub for Pos { 113 | type Output = Self; 114 | fn sub(self, Pos(x, y): Pos) -> Self::Output { 115 | Pos(self.0 - x, self.1 - y) 116 | } 117 | } 118 | 119 | impl Mul for Pos { 120 | type Output = Self; 121 | fn mul(self, x: isize) -> Self::Output { 122 | Pos(self.0 * x as f64, self.1 * x as f64) 123 | } 124 | } 125 | 126 | impl Mul for Pos { 127 | type Output = Self; 128 | fn mul(self, x: f64) -> Self::Output { 129 | Pos(self.0 * x as f64, self.1 * x as f64) 130 | } 131 | } 132 | 133 | impl Neg for Pos { 134 | type Output = Self; 135 | fn neg(self) -> Self { 136 | Self(-self.0, -self.1) 137 | } 138 | } 139 | 140 | impl PartialEq for Pos { 141 | fn eq(&self, other: &Self) -> bool { 142 | self.round() == other.round() 143 | } 144 | } 145 | 146 | impl Eq for Pos {} 147 | 148 | impl Hash for Pos { 149 | fn hash(&self, state: &mut H) { 150 | self.round().hash(state) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 |
Rust (Parallel stripes -- Delaunay)
7 |
8 |
9 | 10 |
Ocean (Parallel waves -- Squares and triangles
11 |
12 |
13 | 14 |
Fire (Free circles -- Hexagons and triangles)
15 |
16 |
17 | 18 |
Forest (Free triangles -- Triangles)
19 |
20 |
21 | 22 |
Sky (Concentric circles -- Hexagons)
23 |
24 |
25 | 26 |
Blood (Crossed stripes -- Hexagons and triangles)
27 |
28 |
29 |

All images are generated as SVG

30 | 31 | --- 32 | 33 |
34 |

35 | 36 | 37 | 38 |

39 |

40 | This project is written in pure Rust and aims to provide a cross-platform utility for generating random abstract wallpapers. It is fast and memory-efficient enough to be able to run in the background at regular intervals 41 |

42 |
43 |
44 |
45 |

46 | 47 | 48 | 49 |

50 |

51 | The full source code is hosted on GitHub, and so is this website. Contributions in any form (pull requests, feature requests, bug reports, etc...) are welcome. 52 |

53 |
54 |
55 | 56 | --- 57 | 58 | # How to install 59 | 60 | ## From source 61 | 62 | Clone the repository and `cd` inside. 63 | 64 | Use `cargo build --release --features nice,set-wallpaper` 65 | [⁽¹⁾](#text-1) 66 | [⁽²⁾](#text-2) 67 | [⁽³⁾](#text-3) 68 | [⁽*⁾](#text-star) 69 | to create the `wallrnd` executable. 70 | 71 | Make sure to put `wallrnd` in your `$PATH`. 72 | 73 | ## From `crates.io` 74 | 75 | run `cargo install wallrnd --features nice,set-wallpaper` 76 | [⁽¹⁾](#text-1) 77 | [⁽²⁾](#text-2) 78 | [⁽³⁾](#text-3) 79 | [⁽*⁾](#text-star) 80 | 81 | Make sure your `$PATH` contains `~/.cargo/bin`. 82 | 83 | 84 | ## And then ? 85 | 86 | Run `wallrnd --image path/to/image.svg --config path/to/configuration.toml --set --nice` 87 | [⁽⁴⁾](#text-4) 88 | [⁽⁵⁾](#text-5) 89 | [⁽⁶⁾](#text-6) to create a new wallpaper. 90 | 91 | A configuration file is provided under `/setup/wallrnd.toml`. 92 | 93 | To generate wallpapers at regular intervals, you can create a new cronjob that calls `wallrnd`. Examples of this are available in `/setup`. 94 | 95 | --- 96 | 97 | # Alternative tools 98 | 99 | #### Online 100 | 101 | * [Random Wallpaper Generator!](http://bjmiller.net/canvas/wallpaper/) 102 | 103 | * [Background Generator](https://bggenerator.com/) 104 | 105 | #### Scripts 106 | 107 | * [flopp/RandomWallpapers](https://github.com/flopp/RandomWallpapers) 108 | 109 | #### Apps 110 | * [Tapet](https://play.google.com/store/apps/details?id=com.sharpregion.tapet&hl=en_US) 111 | 112 | Do you know of another similar tool ? You can suggest it [here](https://github.com/Vanille-N/wallrnd/issues) 113 | 114 | * [(1)](#ref-1) `nice` enables lowering the process priority. It is optionnal. 115 | * [(2)](#ref-2) `set-wallpaper` enables writing a configuration variable to change the current wallpaper. It is optionnal. 116 | * [(3)](#ref-3) `make-png` is also available although not recommended if your system supports svg. 117 | * [(4)](#ref-4) `--set` only available if `set-wallpaper` selected 118 | * [(5)](#ref-5) `--nice` only available if `nice` selected 119 | * [(6)](#ref-3) image extension can be `png` if `make-png` selected 120 | * [(*)](#ref-star) your compiler should be the latest stable release 121 | -------------------------------------------------------------------------------- /src/svg.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::fmt; 3 | use std::io::{self, Write}; 4 | 5 | pub struct Path { 6 | pub stroke_width: f64, 7 | pub stroke_color: Color, 8 | pub fill_color: Color, 9 | pub data: Data, 10 | } 11 | 12 | pub struct Data(pub Vec); 13 | 14 | pub struct Document { 15 | pub frame: Frame, 16 | pub items: Vec, 17 | } 18 | 19 | impl Data { 20 | pub fn new(pos: Pos) -> Self { 21 | Self(vec![pos]) 22 | } 23 | 24 | pub fn line_to(&mut self, pos: Pos) { 25 | self.0.push(pos); 26 | } 27 | 28 | pub fn with_line_to(mut self, pos: Pos) -> Self { 29 | self.0.push(pos); 30 | self 31 | } 32 | } 33 | 34 | impl Path { 35 | pub fn new(d: Data) -> Self { 36 | Self { 37 | stroke_width: 0.0, 38 | stroke_color: Color(0, 0, 0), 39 | fill_color: Color(255, 255, 255), 40 | data: d, 41 | } 42 | } 43 | 44 | pub fn with_fill_color(mut self, c: Color) -> Self { 45 | self.fill_color = c; 46 | self 47 | } 48 | 49 | pub fn with_stroke_color(mut self, c: Color) -> Self { 50 | self.stroke_color = c; 51 | self 52 | } 53 | 54 | pub fn with_stroke_width(mut self, w: f64) -> Self { 55 | self.stroke_width = w; 56 | self 57 | } 58 | } 59 | 60 | impl Document { 61 | pub fn new(frame: Frame) -> Self { 62 | Self { 63 | frame, 64 | items: Vec::new(), 65 | } 66 | } 67 | 68 | pub fn add(&mut self, path: Path) { 69 | self.items.push(path); 70 | } 71 | 72 | pub fn save(&self, dest: &str) -> io::Result<()> { 73 | if dest.ends_with(".svg") || dest.ends_with(".svg.tmp") { 74 | let mut buffer = std::fs::File::create(dest)?; 75 | buffer.write_all(&format!("{}", &self).into_bytes()) 76 | } else if dest.ends_with(".png") || dest.ends_with(".png.tmp") { 77 | #[cfg(feature = "make-png")] 78 | { 79 | // The following code uses functionality from two crates licensed under MPL 2.0 80 | // usvg: https://crates.io/crates/usvg 81 | // resvg: https://crates.io/crates/resvg 82 | let svg_data = format!("{}", &self); 83 | let tree = match usvg::Tree::from_str(&svg_data, &usvg::Options::default()) { 84 | Ok(tree) => tree, 85 | Err(_) => { 86 | return Err(io::Error::new( 87 | io::ErrorKind::InvalidData, 88 | "Failed to parse svg", 89 | )) 90 | } 91 | }; 92 | let fit_to = usvg::FitTo::Original; 93 | let bg = None; 94 | let converted = match resvg::render(&tree, fit_to, bg) { 95 | Some(img) => img, 96 | None => { 97 | return Err(io::Error::new( 98 | io::ErrorKind::InvalidData, 99 | "Failed to convert to png", 100 | )) 101 | } 102 | }; 103 | match converted.save_png(dest) { 104 | Ok(_) => Ok(()), 105 | Err(_) => { 106 | return Err(io::Error::new( 107 | io::ErrorKind::AddrNotAvailable, 108 | "Could not save image", 109 | )) 110 | } 111 | } 112 | } 113 | #[cfg(not(feature = "make-png"))] 114 | { 115 | Err(io::Error::new( 116 | io::ErrorKind::InvalidData, 117 | "PNG is not supported with the current feature flags -- Make sure to include the feature 'make-png' to access this option -- See 'https://doc.rust-lang.org/cargo/reference/features.html' to learn how to do it", 118 | )) 119 | } 120 | } else { 121 | Err(io::Error::new( 122 | io::ErrorKind::InvalidData, 123 | "Can only support .svg and .png extensions", 124 | )) 125 | } 126 | } 127 | } 128 | 129 | impl fmt::Display for Path { 130 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 131 | write!( 132 | f, 133 | "", 134 | self.data, self.fill_color, self.stroke_color, self.stroke_width 135 | ) 136 | } 137 | } 138 | 139 | impl fmt::Display for Data { 140 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 141 | if !self.0.is_empty() { 142 | let Pos(x, y) = self.0[0]; 143 | write!(f, "M{},{} ", x, y)?; 144 | } 145 | for Pos(x, y) in self.0.iter().skip(1) { 146 | write!(f, "L{},{} ", x, y)?; 147 | } 148 | write!(f, "z") 149 | } 150 | } 151 | 152 | impl fmt::Display for Document { 153 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 154 | let (x1, y1, x2, y2) = self.frame.into_tuple(); 155 | let src = String::from("http://www.w3.org/2000/svg"); 156 | writeln!( 157 | f, 158 | "", 159 | x1, y1, x2, y2, src 160 | )?; 161 | for p in self.items.iter() { 162 | writeln!(f, "{}", p)?; 163 | } 164 | write!(f, "") 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/cfg.rs: -------------------------------------------------------------------------------- 1 | use crate::paint::*; 2 | use crate::prelude::*; 3 | use crate::scene::*; 4 | use crate::svg::*; 5 | use crate::tesselate::*; 6 | use rand::{rngs::ThreadRng, seq::SliceRandom, Rng}; 7 | use std::rc::Rc; 8 | 9 | /// General information on a scene 10 | pub struct SceneCfg { 11 | pub theme: Chooser, 12 | pub distance: usize, 13 | pub deviation: usize, 14 | pub frame: Frame, 15 | pub pattern: Pattern, 16 | pub tiling: Tiling, 17 | pub nb_pattern: usize, 18 | pub var_stripes: usize, 19 | pub size_tiling: f64, 20 | pub nb_delaunay: usize, 21 | pub width_pattern: f64, 22 | pub line_width: f64, 23 | pub line_color: Color, 24 | pub tightness_spiral: f64, 25 | } 26 | 27 | /// A trait to box scene items and make them generic. 28 | /// Spares us from a few lines of repeated code. 29 | trait Dynamic 30 | where 31 | C: Contains + 'static, 32 | { 33 | fn dynamic(self) -> Vec>; 34 | } 35 | 36 | impl Dynamic for Vec 37 | where 38 | C: Contains + 'static, 39 | { 40 | fn dynamic(self) -> Vec> { 41 | self.into_iter() 42 | .map(|d| Rc::new(d) as Rc) 43 | .collect::>() 44 | } 45 | } 46 | 47 | impl SceneCfg { 48 | /// Select a random color for a scene item. 49 | /// The actual color will depend on the Chooser with which it is mixed. 50 | pub fn choose_color(&self, rng: &mut ThreadRng) -> ColorItem { 51 | let ThemeItem(c, v, w, salt) = self 52 | .theme 53 | .choose(rng) 54 | .unwrap_or_else(|| ThemeItem(Color(0, 0, 0), None, None, Salt::none())); 55 | ColorItem { 56 | shade: Color::random(rng), 57 | deviation: v.unwrap_or(self.deviation), 58 | distance: w.unwrap_or(self.distance), 59 | theme: c, 60 | salt, 61 | } 62 | } 63 | 64 | /// Match pattern to function that generates it 65 | pub fn create_items(&self, rng: &mut ThreadRng, verbose: Verbosity) -> Vec> { 66 | match self.pattern { 67 | Pattern::FreeCircles => create_free_circles(rng, &self, verbose).dynamic(), 68 | Pattern::FreeTriangles => create_free_triangles(rng, &self, verbose).dynamic(), 69 | Pattern::FreeStripes => create_free_stripes(rng, &self, verbose).dynamic(), 70 | Pattern::FreeSpirals => create_free_spirals(rng, &self, verbose).dynamic(), 71 | Pattern::ConcentricCircles => create_concentric_circles(rng, &self, verbose).dynamic(), 72 | Pattern::ParallelStripes => create_parallel_stripes(rng, &self, verbose).dynamic(), 73 | Pattern::CrossedStripes => create_crossed_stripes(rng, &self, verbose).dynamic(), 74 | Pattern::ParallelWaves => create_waves(rng, &self, verbose).dynamic(), 75 | Pattern::ParallelSawteeth => create_sawteeth(rng, &self, verbose).dynamic(), 76 | } 77 | } 78 | 79 | /// Math tiling to function that generates it 80 | pub fn make_tiling(&self, rng: &mut ThreadRng) -> Vec<(Pos, Path)> { 81 | match self.tiling { 82 | Tiling::Hexagons => tile_hexagons(&self.frame, self.size_tiling, rng.gen_range(0, 360)), 83 | Tiling::Triangles => { 84 | tile_triangles(&self.frame, self.size_tiling, rng.gen_range(0, 360)) 85 | } 86 | Tiling::HexagonsAndTriangles => { 87 | tile_hybrid_hexagons_triangles(&self.frame, self.size_tiling, rng.gen_range(0, 360)) 88 | } 89 | Tiling::SquaresAndTriangles => { 90 | tile_hybrid_squares_triangles(&self.frame, self.size_tiling, rng.gen_range(0, 360)) 91 | } 92 | Tiling::Rhombus => tile_rhombus( 93 | &self.frame, 94 | self.size_tiling, 95 | (rng.gen::() * 0.6 + 0.4) * self.size_tiling, 96 | rng.gen_range(0, 360), 97 | ), 98 | Tiling::Delaunay => random_delaunay(&self.frame, rng, self.nb_delaunay), 99 | Tiling::Pentagons(n) => { 100 | let n = match n { 101 | 0 => rng.gen_range(1, 7), 102 | n => n, 103 | }; 104 | let ptiler = match n { 105 | 1 => pentagons_type1, 106 | 2 => pentagons_type2, 107 | 3 => pentagons_type3, 108 | 4 => pentagons_type4, 109 | 5 => pentagons_type5, 110 | 6 => pentagons_type6, 111 | _ => unreachable!(), 112 | }; 113 | ptiler(&self.frame, self.size_tiling, rng.gen_range(0, 360)) 114 | } 115 | } 116 | } 117 | } 118 | 119 | /// Available patterns, open to additions 120 | #[derive(Debug, Clone, Copy)] 121 | pub enum Pattern { 122 | FreeCircles, 123 | FreeTriangles, 124 | FreeStripes, 125 | FreeSpirals, 126 | ConcentricCircles, 127 | ParallelStripes, 128 | CrossedStripes, 129 | ParallelWaves, 130 | ParallelSawteeth, 131 | } 132 | 133 | impl Pattern { 134 | /// Pick a random pattern (fallback if no other pattern choosing method is specified) 135 | pub fn choose(rng: &mut ThreadRng) -> Self { 136 | use Pattern::*; 137 | *vec![ 138 | FreeCircles, 139 | FreeTriangles, 140 | FreeStripes, 141 | FreeSpirals, 142 | ConcentricCircles, 143 | ParallelStripes, 144 | CrossedStripes, 145 | ParallelWaves, 146 | ParallelSawteeth, 147 | ] 148 | .choose(rng) 149 | .unwrap() 150 | } 151 | } 152 | 153 | ///Available tilings, open to additions 154 | #[derive(Debug, Clone, Copy)] 155 | pub enum Tiling { 156 | Hexagons, 157 | Triangles, 158 | HexagonsAndTriangles, 159 | SquaresAndTriangles, 160 | Rhombus, 161 | Delaunay, 162 | Pentagons(u8), 163 | } 164 | 165 | impl Tiling { 166 | /// Pick a random tiling (fallback if no other tiling choosing method is specified) 167 | pub fn choose(rng: &mut ThreadRng) -> Self { 168 | use Tiling::*; 169 | *vec![ 170 | Hexagons, 171 | Triangles, 172 | HexagonsAndTriangles, 173 | SquaresAndTriangles, 174 | Rhombus, 175 | Delaunay, 176 | Pentagons(0), 177 | ] 178 | .choose(rng) 179 | .unwrap() 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/paint.rs: -------------------------------------------------------------------------------- 1 | use crate::cfg::SceneCfg; 2 | use crate::prelude::*; 3 | use crate::scene::*; 4 | use rand::{rngs::ThreadRng, Rng}; 5 | 6 | pub fn create_free_circles(rng: &mut ThreadRng, cfg: &SceneCfg, verbose: Verbosity) -> Vec { 7 | let mut items = Vec::new(); 8 | for i in 1..=cfg.nb_pattern { 9 | let c = cfg.choose_color(rng); 10 | items.push(Disc::random( 11 | rng, 12 | &cfg.frame, 13 | c, 14 | i as f64 / cfg.nb_pattern as f64 * 0.5, 15 | )); 16 | } 17 | items.sort_by(|a, b| a.radius.partial_cmp(&b.radius).unwrap()); 18 | if verbose.details { 19 | println!("{:#?}", items); 20 | } 21 | items 22 | } 23 | 24 | pub fn create_free_triangles( 25 | rng: &mut ThreadRng, 26 | cfg: &SceneCfg, 27 | verbose: Verbosity, 28 | ) -> Vec { 29 | let mut items = Vec::new(); 30 | for i in 1..=cfg.nb_pattern { 31 | let c = cfg.choose_color(rng); 32 | items.push(Disc::random( 33 | rng, 34 | &cfg.frame, 35 | c, 36 | i as f64 / cfg.nb_pattern as f64 * 0.7, 37 | )); 38 | } 39 | items.sort_by(|a, b| a.radius.partial_cmp(&b.radius).unwrap()); 40 | let items = items 41 | .into_iter() 42 | .map(|d| Triangle::random(rng, d)) 43 | .collect::>(); 44 | if verbose.details { 45 | println!("{:#?}", items); 46 | } 47 | items 48 | } 49 | 50 | pub fn create_free_stripes(rng: &mut ThreadRng, cfg: &SceneCfg, verbose: Verbosity) -> Vec { 51 | let mut items = Vec::new(); 52 | for _ in 0..cfg.nb_pattern { 53 | let c = cfg.choose_color(rng); 54 | let w = cfg.width_pattern * cfg.frame.h as f64 * (rng.gen::() + 0.5); 55 | items.push(Stripe::random(rng, &cfg.frame, c, w)); 56 | } 57 | if verbose.details { 58 | println!("{:#?}", items); 59 | } 60 | items 61 | } 62 | 63 | pub fn create_free_spirals(rng: &mut ThreadRng, cfg: &SceneCfg, verbose: Verbosity) -> Vec { 64 | let mut items = Vec::new(); 65 | for _ in 0..cfg.nb_pattern { 66 | let c = cfg.choose_color(rng); 67 | let w = cfg.width_pattern * cfg.frame.h as f64 * (rng.gen::() + 0.5); 68 | items.push(Spiral::random(rng, &cfg.frame, c, w, cfg.tightness_spiral)); 69 | } 70 | items.sort_by(|a, b| a.width.partial_cmp(&b.width).unwrap()); 71 | if verbose.details { 72 | println!("{:#?}", items); 73 | } 74 | items 75 | } 76 | 77 | pub fn create_concentric_circles( 78 | rng: &mut ThreadRng, 79 | cfg: &SceneCfg, 80 | verbose: Verbosity, 81 | ) -> Vec { 82 | let mut items = Vec::new(); 83 | let center = Pos::random(&cfg.frame, rng); 84 | let d = center 85 | .dist(Pos(0., 0.)) 86 | .max(center.dist(Pos(0., cfg.frame.w as f64))) 87 | .max(center.dist(Pos(cfg.frame.h as f64, 0.))) 88 | .max(center.dist(Pos(cfg.frame.h as f64, cfg.frame.w as f64))); 89 | for i in 1..=cfg.nb_pattern { 90 | items.push(Disc { 91 | center, 92 | radius: d * i as f64 / cfg.nb_pattern as f64, 93 | color: cfg.choose_color(rng), 94 | }) 95 | } 96 | items.sort_by(|a, b| a.radius.partial_cmp(&b.radius).unwrap()); 97 | if verbose.details { 98 | println!("{:#?}", items); 99 | } 100 | items 101 | } 102 | 103 | pub fn create_parallel_stripes( 104 | rng: &mut ThreadRng, 105 | cfg: &SceneCfg, 106 | verbose: Verbosity, 107 | ) -> Vec { 108 | let mut items = Vec::new(); 109 | let (a, b, dir) = { 110 | let c = cfg.frame.center(); 111 | let w = cfg.frame.h + cfg.frame.w; 112 | let dir = rng.gen_range(0, 360); 113 | let d = Pos::polar(dir, w as f64 / 2.); 114 | (c + d, c - d, dir) 115 | }; 116 | for i in 0..=cfg.nb_pattern { 117 | let c = cfg.choose_color(rng); 118 | let p = i as f64 / cfg.nb_pattern as f64; 119 | items.push(HalfPlane::random( 120 | rng, 121 | a * (1. - p) + b * p, 122 | 180 + dir, 123 | cfg.var_stripes, 124 | c, 125 | )); 126 | } 127 | if verbose.details { 128 | println!("{:#?}", items); 129 | } 130 | items 131 | } 132 | 133 | pub fn create_crossed_stripes( 134 | rng: &mut ThreadRng, 135 | cfg: &SceneCfg, 136 | verbose: Verbosity, 137 | ) -> Vec { 138 | let mut items = Vec::new(); 139 | let (a, b, a_orth, b_orth, dir) = { 140 | let c = cfg.frame.center(); 141 | let w = cfg.frame.h + cfg.frame.w; 142 | let dir = rng.gen_range(0, 360); 143 | let d = Pos::polar(dir, w as f64 / 2.); 144 | let d_orth = Pos::polar(dir + 90, w as f64 / 2.); 145 | (c + d, c - d, c - d_orth, c + d_orth, dir) 146 | }; 147 | for i in 0..=cfg.nb_pattern { 148 | let p = i as f64 / cfg.nb_pattern as f64; 149 | let c = cfg.choose_color(rng); 150 | items.push(HalfPlane::random( 151 | rng, 152 | a * (1. - p) + b * p, 153 | 180 + dir, 154 | cfg.var_stripes, 155 | c, 156 | )); 157 | let c = cfg.choose_color(rng); 158 | items.push(HalfPlane::random( 159 | rng, 160 | a_orth * (1. - p) + b_orth * p, 161 | 90 + dir, 162 | cfg.var_stripes, 163 | c, 164 | )); 165 | } 166 | if verbose.details { 167 | println!("{:#?}", items); 168 | } 169 | items 170 | } 171 | 172 | pub fn create_waves(rng: &mut ThreadRng, cfg: &SceneCfg, verbose: Verbosity) -> Vec { 173 | let mut items = Vec::new(); 174 | let (a, b, dir) = { 175 | let c = cfg.frame.center(); 176 | let w = cfg.frame.h + cfg.frame.w; 177 | let dir = rng.gen_range(0, 360); 178 | let d = Pos::polar(dir, w as f64 / 2.); 179 | (c + d, c - d, dir) 180 | }; 181 | let amplitude = (b - a).norm() / cfg.nb_pattern as f64 / 2.; 182 | for i in 0..=cfg.nb_pattern { 183 | let c = cfg.choose_color(rng); 184 | let p = i as f64 / cfg.nb_pattern as f64; 185 | items.push(Wave::random( 186 | rng, 187 | a * (1. - p) + b * p, 188 | 180 + dir, 189 | cfg.width_pattern / 5., 190 | amplitude, 191 | c, 192 | )); 193 | } 194 | if verbose.details { 195 | println!("{:#?}", items); 196 | } 197 | items 198 | } 199 | 200 | pub fn create_sawteeth(rng: &mut ThreadRng, cfg: &SceneCfg, verbose: Verbosity) -> Vec { 201 | let mut items = Vec::new(); 202 | let (a, b, dir) = { 203 | let c = cfg.frame.center(); 204 | let w = cfg.frame.h + cfg.frame.w; 205 | let dir = rng.gen_range(0, 360); 206 | let d = Pos::polar(dir, w as f64 / 2.); 207 | (c + d, c - d, dir) 208 | }; 209 | let amplitude = (b - a).norm() / cfg.nb_pattern as f64 / 2.; 210 | for i in 0..=cfg.nb_pattern { 211 | let c = cfg.choose_color(rng); 212 | let p = i as f64 / cfg.nb_pattern as f64; 213 | items.push(Sawtooth::random( 214 | rng, 215 | a * (1. - p) + b * p, 216 | 180 + dir, 217 | cfg.width_pattern / 5., 218 | amplitude, 219 | c, 220 | )); 221 | } 222 | if verbose.details { 223 | println!("{:#?}", items); 224 | } 225 | items 226 | } 227 | -------------------------------------------------------------------------------- /src/scene.rs: -------------------------------------------------------------------------------- 1 | use crate::cfg::SceneCfg; 2 | use crate::pos::crossprod_sign; 3 | use crate::prelude::*; 4 | use rand::{rngs::ThreadRng, Rng}; 5 | use std::rc::Rc; 6 | 7 | pub struct Scene { 8 | pub bg: ColorItem, 9 | pub items: Vec>, 10 | } 11 | 12 | impl Scene { 13 | pub fn new(cfg: &SceneCfg, rng: &mut ThreadRng, verbose: Verbosity) -> Self { 14 | Self { 15 | bg: cfg.choose_color(rng), 16 | items: cfg.create_items(rng, verbose), 17 | } 18 | } 19 | 20 | /// Get color of a position depending on objects that were hit 21 | pub fn color(&self, p: Pos, rng: &mut ThreadRng) -> Color { 22 | for i in &self.items { 23 | if let Some(c) = i.contains(p, rng) { 24 | return c; 25 | } 26 | } 27 | self.bg.sample(rng) 28 | } 29 | } 30 | 31 | /// Trait for anything that can contain a 2D point 32 | pub trait Contains: std::fmt::Display { 33 | fn contains(&self, p: Pos, rng: &mut ThreadRng) -> Option; 34 | } 35 | 36 | #[derive(Debug, Clone)] 37 | pub struct ColorItem { 38 | pub shade: Color, 39 | pub deviation: usize, 40 | pub theme: Color, 41 | pub distance: usize, 42 | pub salt: Salt, 43 | } 44 | 45 | impl ColorItem { 46 | pub fn sample(&self, rng: &mut ThreadRng) -> Color { 47 | self.salt.sample(rng).unwrap_or_else(|| { 48 | self.shade 49 | .meanpoint(self.theme, self.distance) 50 | .variate(rng, self.deviation) 51 | }) 52 | } 53 | } 54 | 55 | #[derive(Debug)] 56 | pub struct Disc { 57 | pub center: Pos, 58 | pub radius: f64, 59 | pub color: ColorItem, 60 | } 61 | 62 | impl Disc { 63 | pub fn random(rng: &mut ThreadRng, f: &Frame, color: ColorItem, size_hint: f64) -> Self { 64 | let center = Pos::random(f, rng); 65 | let radius = (rng.gen::() * size_hint + 0.1) * (f.h.min(f.w) as f64); 66 | Self { 67 | center, 68 | radius, 69 | color, 70 | } 71 | } 72 | } 73 | 74 | impl Contains for Disc { 75 | fn contains(&self, p: Pos, rng: &mut ThreadRng) -> Option { 76 | if (self.center - p).dot_self() < self.radius.powi(2) { 77 | Some(self.color.sample(rng)) 78 | } else { 79 | None 80 | } 81 | } 82 | } 83 | 84 | #[derive(Debug)] 85 | pub struct HalfPlane { 86 | pub limit: Pos, 87 | pub reference: Pos, 88 | pub color: ColorItem, 89 | } 90 | 91 | impl HalfPlane { 92 | pub fn random( 93 | rng: &mut ThreadRng, 94 | limit: Pos, 95 | indic: isize, 96 | var: usize, 97 | color: ColorItem, 98 | ) -> Self { 99 | Self { 100 | limit, 101 | reference: limit 102 | + Pos::polar( 103 | rng.gen_range(indic - var as isize, indic + var as isize), 104 | 100., 105 | ), 106 | color, 107 | } 108 | } 109 | } 110 | 111 | impl Contains for HalfPlane { 112 | fn contains(&self, p: Pos, rng: &mut ThreadRng) -> Option { 113 | let dotprod = (p - self.limit).dot(self.reference - self.limit); 114 | if dotprod < 0. { 115 | Some(self.color.sample(rng)) 116 | } else { 117 | None 118 | } 119 | } 120 | } 121 | 122 | #[derive(Debug)] 123 | pub struct Triangle { 124 | pub a: Pos, 125 | pub b: Pos, 126 | pub c: Pos, 127 | pub color: ColorItem, 128 | } 129 | 130 | impl Triangle { 131 | pub fn random(rng: &mut ThreadRng, circ: Disc) -> Self { 132 | let theta0 = rng.gen_range(0, 360); 133 | let theta1 = rng.gen_range(80, 150); 134 | let theta2 = rng.gen_range(80, 150); 135 | Self { 136 | a: circ.center + Pos::polar(theta0, circ.radius), 137 | b: circ.center + Pos::polar(theta0 + theta1, circ.radius), 138 | c: circ.center + Pos::polar(theta0 + theta1 + theta2, circ.radius), 139 | color: circ.color, 140 | } 141 | } 142 | } 143 | 144 | impl Contains for Triangle { 145 | fn contains(&self, p: Pos, rng: &mut ThreadRng) -> Option { 146 | let d1 = crossprod_sign(p, self.a, self.b); 147 | let d2 = crossprod_sign(p, self.b, self.c); 148 | let d3 = crossprod_sign(p, self.c, self.a); 149 | let has_pos = d1 || d2 || d3; 150 | let has_neg = !(d1 && d2 && d3); 151 | if !(has_neg && has_pos) { 152 | Some(self.color.sample(rng)) 153 | } else { 154 | None 155 | } 156 | } 157 | } 158 | 159 | #[derive(Debug)] 160 | pub struct Spiral { 161 | pub center: Pos, 162 | pub width: f64, 163 | pub tightness: f64, 164 | pub color: ColorItem, 165 | } 166 | 167 | impl Spiral { 168 | pub fn random( 169 | rng: &mut ThreadRng, 170 | f: &Frame, 171 | color: ColorItem, 172 | width: f64, 173 | tightness: f64, 174 | ) -> Self { 175 | Self { 176 | center: Pos::random(f, rng), 177 | width, 178 | color, 179 | tightness, 180 | } 181 | } 182 | } 183 | 184 | trait FracPart { 185 | fn frac_part(self) -> Self; 186 | } 187 | 188 | impl FracPart for f64 { 189 | fn frac_part(self) -> Self { 190 | self - self.floor() 191 | } 192 | } 193 | 194 | impl Contains for Spiral { 195 | fn contains(&self, p: Pos, rng: &mut ThreadRng) -> Option { 196 | let Pos(di, dj) = self.center - p; 197 | let theta = di.atan2(dj); 198 | let radius = (di.powi(2) + dj.powi(2)).sqrt() + theta / std::f64::consts::PI * self.width; 199 | if (radius / self.width).frac_part() < self.tightness { 200 | Some(self.color.sample(rng)) 201 | } else { 202 | None 203 | } 204 | } 205 | } 206 | 207 | #[derive(Debug)] 208 | pub struct Stripe { 209 | pub limit: Pos, 210 | pub reference: Pos, 211 | pub color: ColorItem, 212 | } 213 | 214 | impl Stripe { 215 | pub fn random(rng: &mut ThreadRng, f: &Frame, color: ColorItem, width: f64) -> Self { 216 | let limit = Pos::random(f, rng); 217 | let reference = limit + Pos::polar(rng.gen_range(0, 360), width); 218 | Self { 219 | limit, 220 | reference, 221 | color, 222 | } 223 | } 224 | } 225 | 226 | impl Contains for Stripe { 227 | fn contains(&self, p: Pos, rng: &mut ThreadRng) -> Option { 228 | let dotprod1 = (p - self.limit).dot(self.reference - self.limit); 229 | let dotprod2 = (p - self.reference).dot(self.limit - self.reference); 230 | if dotprod1 > 0. && dotprod2 > 0. { 231 | Some(self.color.sample(rng)) 232 | } else { 233 | None 234 | } 235 | } 236 | } 237 | 238 | #[derive(Debug)] 239 | pub struct Wave { 240 | pub limit: Pos, 241 | pub reference: Pos, 242 | pub amplitude: f64, 243 | pub frequency: f64, 244 | pub color: ColorItem, 245 | } 246 | 247 | impl Wave { 248 | pub fn random( 249 | _rng: &mut ThreadRng, 250 | limit: Pos, 251 | indic: isize, 252 | width: f64, 253 | amplitude: f64, 254 | color: ColorItem, 255 | ) -> Self { 256 | Self { 257 | limit, 258 | reference: limit + Pos::polar(indic, 100.), 259 | amplitude, 260 | frequency: 0.001 / width, 261 | color, 262 | } 263 | } 264 | } 265 | 266 | impl Contains for Wave { 267 | fn contains(&self, p: Pos, rng: &mut ThreadRng) -> Option { 268 | let proj = (p - self.limit).project(self.reference - self.limit); 269 | let nearpt = p - proj; 270 | let phase = (self.limit - nearpt).norm() * self.frequency; 271 | if phase.cos() * self.amplitude > (p - self.limit).dot((self.reference - self.limit).unit()) 272 | { 273 | Some(self.color.sample(rng)) 274 | } else { 275 | None 276 | } 277 | } 278 | } 279 | 280 | #[derive(Debug)] 281 | pub struct Sawtooth { 282 | pub limit: Pos, 283 | pub reference: Pos, 284 | pub amplitude: f64, 285 | pub frequency: f64, 286 | pub color: ColorItem, 287 | } 288 | 289 | impl Sawtooth { 290 | pub fn random( 291 | _rng: &mut ThreadRng, 292 | limit: Pos, 293 | indic: isize, 294 | width: f64, 295 | amplitude: f64, 296 | color: ColorItem, 297 | ) -> Self { 298 | Self { 299 | limit, 300 | reference: limit + Pos::polar(indic, 100.), 301 | amplitude, 302 | frequency: 0.0007 / width, 303 | color, 304 | } 305 | } 306 | } 307 | 308 | impl Contains for Sawtooth { 309 | fn contains(&self, p: Pos, rng: &mut ThreadRng) -> Option { 310 | let sawtooth = |f: f64| { 311 | let int = f.floor(); 312 | let frac = f - int; 313 | match (int as i32).rem_euclid(4) { 314 | 0 => 1. - frac, 315 | 1 => -frac, 316 | 2 => frac - 1., 317 | 3 => frac, 318 | _ => unreachable!(), 319 | } 320 | }; 321 | let proj = (p - self.limit).project(self.reference - self.limit); 322 | let nearpt = p - proj; 323 | let phase = (self.limit - nearpt).norm() * self.frequency; 324 | if sawtooth(phase) * self.amplitude 325 | > (p - self.limit).dot((self.reference - self.limit).unit()) 326 | { 327 | Some(self.color.sample(rng)) 328 | } else { 329 | None 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wallrnd 2 | ### A configurable generator of random abstract time-aware wallpapers 3 | 4 | [![](https://img.shields.io/badge/github-Vanille--N/wallrnd-8da0cb?logo=github)](https://github.com/Vanille-N/wallrnd) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | [![crates.io](http://meritbadge.herokuapp.com/wallrnd)](https://crates.io/crates/wallrnd) 8 | [![API](https://docs.rs/wallrnd/badge.svg)](https://docs.rs/wallrnd) 9 | 10 | Note: `wallrnd` is stalled as I have stopped using it personally, but I still accept feature requests. 11 | 12 | --- 13 | 14 | This project aims to provide a utility for generating random abstract wallpapers. 15 | 16 | Until recently it could only generate the images, not actually set them as wallpapers. This limitation is being addressed. 17 | 18 | A bash script to automatically change wallpaper is provided, and has been tested on Ubuntu 20.04 (Focal Fossa). 19 | Portability of this script is outside of the scope of this project (the image generator however should be portable to any OS), but scripts that work for other shells/distros are welcome. 20 | 21 | --- 22 | 23 | ## Recommended setup (executable `wallrnd`) 24 | 25 | * `cargo install wallrnd --features set-wallpaper,nice` 26 | 27 | * Make a copy of `setup/wallrnd.toml` and adjust the settings to your liking 28 | 29 | * Make a new Crontab entry: `* * * * * wallrnd --config /path/to/wallrnd.toml --image /tmp/wallpaper.svg --set --nice` 30 | 31 | The `--nice` option causes `wallrnd` to lower its priority as soon as launched, which prevents the user from experiencing a short delay each time a new wallpaper is generated. 32 | 33 | ## Recommended setup (executable `prototype/prototype.py`) 34 | 35 | `wallrnd` is a more developed product than this, but if you still want to use the prototype it is possible. 36 | 37 | Be warned that performance is a lot worse and that this version is far less configurable. 38 | 39 | * Edit `prototype/set-wallpaper` to your liking, make it executable, and add it to your `$PATH`. 40 | 41 | * Make a new Crontab entry: `* * * * * set-wallpaper` 42 | 43 | [`psutil`](https://pypi.org/project/psutil/) is used to abort the process if CPU usage is already high. 44 | 45 | 46 | ## Advanced setup 47 | 48 | You may be interested in these other setup methods if 49 | - your OS does not support setting an SVG image as wallpaper 50 | - you do not wish to use GPL- or MPL-licensed products 51 | - your OS is not included in [this list of supported environments](https://github.com/vineetred/flowy#supported-environments) 52 | - you want custom functionality such as aborting the script when running on battery 53 | - you want to build from source 54 | - `scrummage` (the crate that `wallrnd` depends on to provide the `nice` feature) is not yet compatible with your OS 55 | 56 | ### Installation: 57 | * If you do not need `wallrnd` to set wallpapers, then do not use the feature `set-wallaper`: `cargo install wallrnd`. The same is true if you don't want to use the `nice` feature. 58 | 59 | * If you want to be able to create png images, then you should add the `make-png` feature: `cargo install wallrnd --features make-png` 60 | 61 | * To have all features, you can use `cargo install wallrnd --features all` 62 | 63 | * You can also build from source: 64 | ``` 65 | git clone https://github.com/Vanille-N/wallrnd 66 | cd wallrnd 67 | cargo build --release --features nice,set-wallpaper 68 | cp target/release/wallrnd ~/bin/wallrnd 69 | ``` 70 | 71 | ### Configuration 72 | * `setup/wallrnd.toml` includes examples and explanations on how to setup options. Feel free to ask for more explanations. 73 | 74 | * The configuration file doesn't have to be named `wallrnd.toml`, but it has to be formatted like a TOML file. 75 | 76 | ### Automation 77 | * `setup/set-wallpaper-*` are examples of how to set wallrnd to be executed. 78 | 79 | * The appropriate version should be put in your path and executed whenever necessary by adding an entry to your Crontab. `* * * * * set-wallpaper` 80 | 81 | * Note that the file path does not have to be absolute. `wallrnd` resolves paths before writing the wallpaper to a file. 82 | 83 | --- 84 | 85 | ## Features and licensing 86 | 87 | While the code in this crate is licensed under the [MIT license](https://opensource.org/licenses/MIT), the binary target includes (purely for user convenience) dependencies that have more restrictive licenses. 88 | 89 | [resvg](https://crates.io/crates/resvg) and [usvg](https://crates.io/usvg) require the [MPL 2.0 license](https://opensource.org/licenses/MPL-2.0) and pull in some other dependencies under the [BSD 3-clause](https://opensource.org/licenses/BSD-3-Clause). 90 | 91 | [wallpaper_rs](https://crates.io/crates/wallpaper_rs) is licensed under [GPL 3.0](https://opensource.org/licenses/GPL-3.0) 92 | 93 | The features provided by these crates are purely optional. The different features available are explained in more detail in the [Advanced setup](#advanced-setup) section. 94 | 95 | Using the `make-png` feature requires MPL 2.0 or a compatible license. 96 | 97 | Using the `set-wallpaper` feature requires GPL 3.0 or a compatible license. 98 | 99 | Using both requires GPL 3.0 at least. 100 | 101 | The inclusion of MPL- and GPL-licensed crates as dependencies of this crate licensed under MIT does not grant to anyone the right to distribute executables that were compiled using the corresponding feature flags under non-GPL-compatible licenses. Any derivative work that does not include these flags can safely be provided under the MIT license. 102 | 103 | It is not recommended for any crates dependent on this one to use the feature flags, as the functionality obtained from the GPL dependencies is not reexported by wallrnd and thus adds needless dependencies. 104 | 105 | --- 106 | 107 | Direct dependencies 108 | 109 | | dependency | crate | docs | 110 | |-------------------|------------------------------------|---------------------------------------| 111 | | `serde` | [![][serde_cb]][serde_c] | [![API][serde_db]][serde_d] | 112 | | `serde_derive` | [![][derive_cb]][derive_c] | [![API][derive_db]][derive_d] | 113 | | `rand` | [![][rand_cb]][rand_c] | [![API][rand_db]][rand_d] | 114 | | `chrono` | [![][chrono_cb]][chrono_c] | [![API][chrono_db]][chrono_d] | 115 | | `delaunator` | [![][delaunator_cb]][delaunator_c] | [![API][delaunator_db]][delaunator_d] | 116 | | `toml` | [![][toml_cb]][toml_c] | [![API][toml_db]][toml_d] | 117 | | `resvg` * | [![][resvg_cb]][resvg_c] | [![API][resvg_db]][resvg_d] | 118 | | `usvg` * | [![][usvg_cb]][usvg_c] | [![API][usvg_db]][usvg_d] | 119 | | `wallpaper_rs` * | [![][wallpaper_cb]][wallpaper_c] | [![API][wallpaper_db]][wallpaper_d] | 120 | | `scrummage` * | [![][scrummage_cb]][scrummage_c] | [![API][scrummage_db]][scrummage_d] | 121 | 122 | \* Optional dependencies 123 | 124 | 125 | [serde_c]: https://crates.io/crates/serde 126 | [serde_cb]: https://meritbadge.herokuapp.com/serde 127 | [serde_d]: https://docs.rs/serde 128 | [serde_db]: https://docs.rs/serde/badge.svg 129 | 130 | [derive_c]: https://crates.io/crates/serde_derive 131 | [derive_cb]: https://meritbadge.herokuapp.com/serde_derive 132 | [derive_d]: https://docs.rs/serde_derive 133 | [derive_db]: https://docs.rs/serde_derive/badge.svg 134 | 135 | [rand_c]: https://crates.io/crates/rand 136 | [rand_cb]: https://meritbadge.herokuapp.com/rand 137 | [rand_d]: https://docs.rs/rand 138 | [rand_db]: https://docs.rs/rand/badge.svg 139 | 140 | [toml_c]: https://crates.io/crates/toml 141 | [toml_cb]: https://meritbadge.herokuapp.com/toml 142 | [toml_d]: https://docs.rs/toml 143 | [toml_db]: https://docs.rs/toml/badge.svg 144 | 145 | [chrono_c]: https://crates.io/crates/chrono 146 | [chrono_cb]: https://meritbadge.herokuapp.com/chrono 147 | [chrono_d]: https://docs.rs/chrono 148 | [chrono_db]: https://docs.rs/chrono/badge.svg 149 | 150 | [delaunator_c]: https://crates.io/crates/delaunator 151 | [delaunator_cb]: https://meritbadge.herokuapp.com/delaunator 152 | [delaunator_d]: https://docs.rs/delaunator 153 | [delaunator_db]: https://docs.rs/delaunator/badge.svg 154 | 155 | [resvg_c]: https://crates.io/crates/resvg 156 | [resvg_cb]: https://meritbadge.herokuapp.com/resvg 157 | [resvg_d]: https://docs.rs/resvg 158 | [resvg_db]: https://docs.rs/resvg/badge.svg 159 | 160 | [usvg_c]: https://crates.io/crates/usvg 161 | [usvg_cb]: https://meritbadge.herokuapp.com/usvg 162 | [usvg_d]: https://docs.rs/usvg 163 | [usvg_db]: https://docs.rs/usvg/badge.svg 164 | 165 | [wallpaper_c]: https://crates.io/crates/wallpaper_rs 166 | [wallpaper_cb]: https://meritbadge.herokuapp.com/wallpaper_rs 167 | [wallpaper_d]: https://docs.rs/wallpaper_rs 168 | [wallpaper_db]: https://docs.rs/wallpaper_rs/badge.svg 169 | 170 | [scrummage_c]: https://crates.io/crates/scrummage 171 | [scrummage_cb]: https://meritbadge.herokuapp.com/scrummage 172 | [scrummage_d]: https://docs.rs/scrummage 173 | [scrummage_db]: https://docs.rs/scrummage/badge.svg 174 | 175 | --- 176 | 177 | ## Alternative tools 178 | 179 | ### Online 180 | 181 | * [Random Wallpaper Generator!](http://bjmiller.net/canvas/wallpaper/) 182 | 183 | * [Background Generator](https://bggenerator.com/) 184 | 185 | ### Scripts 186 | 187 | * [flopp/RandomWallpapers](https://github.com/flopp/RandomWallpapers) 188 | 189 | * [qryxip/sky-color-wallpaper](https://crates.io/crates/sky-color-wallpaper) 190 | 191 | ### Apps 192 | * [Tapet](https://play.google.com/store/apps/details?id=com.sharpregion.tapet&hl=en_US) 193 | 194 | ## Examples 195 | 196 | As a random generator of wallpaper ought to provide images of consistent quality, the following sample of images is **unfiltered**[\*](#methodology). All were created with a configuration file similar to the one provided under `setup/wallrnd.toml`. 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | [\*](#return-methodology) To provide a variety of patterns, tilings, and themes, the six were created in succession by altering the configuration file slightly so that only one pattern, tiling, and theme was available. This method guarantees variability without biasing quality. Hence the above sample can be considered representative of the general quality of generated wallpapers. 207 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Local, Timelike}; 2 | use std::env; 3 | use std::fs::File; 4 | use std::io::prelude::*; 5 | use std::process::exit; 6 | use wallrnd::deserializer::MetaConfig; 7 | use wallrnd::log::Logger; 8 | use wallrnd::prelude::*; 9 | use wallrnd::scene::Scene; 10 | use wallrnd::svg::*; 11 | 12 | fn main() { 13 | let args = read_command_line_arguments(); 14 | 15 | if args.help { 16 | print_help(); 17 | exit(0); 18 | } 19 | 20 | let verbose = args.verbose; 21 | 22 | if args.nice { 23 | #[cfg(feature = "nice")] 24 | reduce_priority(verbose); 25 | #[cfg(not(feature = "nice"))] 26 | { 27 | if verbose.warn { 28 | println!("Feature 'nice' is not enabled, you cannot control process priority"); 29 | } 30 | exit(1); 31 | } 32 | } 33 | 34 | if args.init != "" { 35 | if verbose.prog { 36 | println!("Initializing configuration file"); 37 | } 38 | make_config_file(&args.init[..]); 39 | exit(0); 40 | } 41 | 42 | // Get local time and convert to app-specific format: HHMM 43 | if verbose.prog { 44 | println!("Reading time"); 45 | } 46 | let time = args.time.unwrap_or_else(|| { 47 | let now = Local::now(); 48 | let h = now.hour(); 49 | let m = now.minute(); 50 | let current = (h * 100 + m) as usize; 51 | if verbose.info { 52 | println!("Using current time: {}", current); 53 | } 54 | current 55 | }); 56 | let dest = args.image; 57 | let fname = args.config; 58 | 59 | if verbose.prog { 60 | println!("Creating random number generator"); 61 | } 62 | let mut rng = rand::thread_rng(); 63 | if verbose.prog { 64 | println!("Attempting to open configuration file"); 65 | } 66 | let cfg_file = File::open(fname); 67 | let mut cfg_contents = String::new(); 68 | if let Ok(mut f) = cfg_file { 69 | if let Err(e) = f.read_to_string(&mut cfg_contents) { 70 | if verbose.warn { 71 | println!("{}; Switching to default settings.", e); 72 | } 73 | } 74 | } else if verbose.warn { 75 | println!("Settings file not found"); 76 | } 77 | if verbose.prog { 78 | println!("Choosing random settings according to configuration"); 79 | } 80 | let mut cfg = MetaConfig::from_string(cfg_contents, verbose).pick_cfg(&mut rng, time, verbose); 81 | 82 | if verbose.prog { 83 | println!("Building scene"); 84 | } 85 | let mut scene = Scene::new(&cfg, &mut rng, verbose); 86 | let stroke = cfg.line_color; 87 | let stroke_width = cfg.line_width; 88 | let stroke_like_fill = stroke_width < 0.0001; 89 | 90 | if args.load != "" { 91 | let loader = Logger::load(&args.load); 92 | let Logger { bg, objects, frame } = loader; 93 | scene.bg = bg; 94 | scene.items = objects; 95 | cfg.frame = frame; 96 | } 97 | 98 | if args.log != "" { 99 | let logger = Logger { 100 | bg: scene.bg.clone(), 101 | objects: scene.items.clone(), 102 | frame: cfg.frame, 103 | }; 104 | logger.save(&args.log).unwrap_or_else(|_| { 105 | if verbose.warn { 106 | println!("No valid destination specified"); 107 | } 108 | }); 109 | } 110 | 111 | // Generate document 112 | if verbose.prog { 113 | println!("Creating tiling"); 114 | } 115 | let mut document = Document::new(cfg.frame); 116 | for (pos, elem) in cfg.make_tiling(&mut rng) { 117 | let fill = scene.color(pos, &mut rng); 118 | document.add( 119 | elem.with_fill_color(fill) 120 | .with_stroke_color(if stroke_like_fill { fill } else { stroke }) 121 | .with_stroke_width(stroke_width.max(0.1)), 122 | ); 123 | } 124 | 125 | if dest == "" { 126 | if verbose.prog { 127 | println!("No destination specified"); 128 | } 129 | exit(1); 130 | } 131 | 132 | if verbose.prog { 133 | println!("Writing image to file"); 134 | } 135 | document.save(&(dest.clone() + ".tmp")).unwrap_or_else(|e| { 136 | if verbose.warn { 137 | println!("An error occured: {:?}", e); 138 | } 139 | exit(1); 140 | }); 141 | #[allow(clippy::redundant_clone)] 142 | // Reason: clone is NOT redundant when certain feature flags are used... 143 | std::process::Command::new("mv") 144 | .arg(format!("{}.tmp", &dest)) 145 | .arg(dest.clone()) 146 | .status() 147 | .unwrap_or_else(|e| { 148 | if verbose.warn { 149 | println!("An error occured: {}", e); 150 | } 151 | exit(1); 152 | }); 153 | if args.set { 154 | #[cfg(feature = "set-wallpaper")] 155 | { 156 | // The following code includes functionality from a crate licensed under GPL 3.0 157 | // wallpaper_rs: https://crates.io/crates/wallpaper_rs 158 | if verbose.prog { 159 | println!("Setting as wallpaper"); 160 | } 161 | use wallpaper_rs::{Desktop, DesktopEnvt}; 162 | let envt = DesktopEnvt::new().unwrap_or_else(|_| { 163 | if verbose.warn { 164 | println!("Unable to detect desktop environment"); 165 | } 166 | exit(1); 167 | }); 168 | let imgdir = std::path::PathBuf::from(&dest); 169 | let canon = std::fs::canonicalize(&imgdir) 170 | .unwrap_or_else(|_| { 171 | if verbose.warn { 172 | println!("Could not resolve path"); 173 | } 174 | exit(1); 175 | }) 176 | .into_os_string() 177 | .into_string() 178 | .unwrap_or_else(|_| { 179 | if verbose.warn { 180 | println!("Invalid file name"); 181 | } 182 | exit(1); 183 | }); 184 | if verbose.info { 185 | println!("File path resolved to '{}'", &canon); 186 | } 187 | envt.set_wallpaper(&canon).unwrap_or_else(|e| { 188 | if verbose.warn { 189 | println!("Could not set as wallpaper"); 190 | println!("Message: {}", e); 191 | } 192 | }); 193 | } 194 | #[cfg(not(feature = "set-wallpaper"))] 195 | { 196 | if verbose.warn { 197 | println!("You have not selected the set-wallpaper functionality"); 198 | println!("Make sure to include the feature 'set-wallpaper' to access this option"); 199 | println!("See 'https://doc.rust-lang.org/cargo/reference/features.html' to learn how to do it"); 200 | } 201 | exit(1); 202 | } 203 | } 204 | if verbose.prog { 205 | println!("Process exited successfully"); 206 | } 207 | } 208 | 209 | #[derive(Default)] 210 | struct Args { 211 | help: bool, 212 | set: bool, 213 | nice: bool, 214 | verbose: Verbosity, 215 | time: Option, 216 | log: String, 217 | load: String, 218 | image: String, 219 | config: String, 220 | init: String, 221 | } 222 | 223 | fn read_command_line_arguments() -> Args { 224 | let mut args = Args::default(); 225 | let args_split = env::args().collect::>(); 226 | let mut it = args_split.iter().skip(1).flat_map(|s| s.split('=')); 227 | 228 | loop { 229 | match it.next().as_deref() { 230 | None => return args, 231 | Some("--help") => args.help = true, 232 | Some("--log") => { 233 | args.log = it 234 | .next() 235 | .unwrap_or_else(|| { 236 | panic!("Option --log should be followed by a destination file") 237 | }) 238 | .to_string() 239 | } 240 | Some("--load") => { 241 | args.load = it 242 | .next() 243 | .unwrap_or_else(|| { 244 | panic!("Option --load should be followed by a source file") 245 | }) 246 | .to_string() 247 | } 248 | Some("--verbose") => args.verbose = Verbosity::from(&it.next().unwrap_or_else(|| panic!("Option --verbose should be followed by a verbosity descriptor: '^[PDIWA]*$', 249 | P: Progress 250 | D: Details 251 | I: Info 252 | W: Warnings 253 | A: All"))[..]), 254 | Some("--init") => { 255 | args.init = it 256 | .next() 257 | .unwrap_or_else(|| panic!("Option --init should be followed by a source file")) 258 | .to_string() 259 | } 260 | Some("--time") => { 261 | args.time = Some( 262 | it.next() 263 | .unwrap_or_else(|| { 264 | panic!("Option --time should be followed by a timestamp.") 265 | }) 266 | .parse() 267 | .unwrap_or_else(|e| panic!("Failed to parse time: {}", e)), 268 | ) 269 | } 270 | Some("--image") => { 271 | args.image = it 272 | .next() 273 | .unwrap_or_else(|| { 274 | panic!("Option --image should be followed by a destination file") 275 | }) 276 | .to_string() 277 | } 278 | Some("--config") => { 279 | args.config = it 280 | .next() 281 | .unwrap_or_else(|| { 282 | panic!("Option --config should be followed by a source file") 283 | }) 284 | .to_string() 285 | } 286 | Some("--set") => args.set = true, 287 | Some("--nice") => args.nice = true, 288 | Some(o) => panic!("Unknown option {}", o), 289 | } 290 | } 291 | } 292 | 293 | fn print_help() { 294 | print!(include_str!("../assets/man")); 295 | } 296 | 297 | fn make_config_file(fname: &str) { 298 | let mut buffer = std::fs::File::create(fname).unwrap_or_else(|e| { 299 | println!("Error creating configuration: {}", e); 300 | exit(1); 301 | }); 302 | let sample_cfg = include_str!("../assets/default.toml"); 303 | buffer 304 | .write_all(&sample_cfg.to_string().into_bytes()) 305 | .unwrap_or_else(|e| { 306 | println!("Error writing configuration: {}", e); 307 | exit(1); 308 | }); 309 | } 310 | 311 | #[cfg(feature = "nice")] 312 | fn reduce_priority(verbose: Verbosity) { 313 | use scrummage::*; 314 | let base = Process::current().priority().unwrap(); 315 | if verbose.info { 316 | println!("Current priority: {:?}", base); 317 | } 318 | let background_priority = base.lower().next().unwrap_or_else(|| { 319 | if verbose.warn { 320 | println!("No lower priority available"); 321 | } 322 | exit(1); 323 | }); 324 | Process::current() 325 | .set_priority(background_priority) 326 | .unwrap_or_else(|_| { 327 | if verbose.warn { 328 | println!("Failed to lower priority"); 329 | } 330 | exit(1); 331 | }); 332 | if verbose.info { 333 | println!( 334 | "Changed priority of current process: {:?}", 335 | Process::current().priority().unwrap() 336 | ); 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::scene::*; 3 | use std::fmt; 4 | use std::fs::File; 5 | use std::io::{Read, Write}; 6 | use std::rc::Rc; 7 | 8 | pub struct Logger { 9 | pub frame: Frame, 10 | pub bg: ColorItem, 11 | pub objects: Vec>, 12 | } 13 | 14 | impl fmt::Display for Frame { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | write!(f, "{} {} {} {} #", self.x, self.y, self.w, self.h) 17 | } 18 | } 19 | 20 | impl fmt::Display for ColorItem { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 22 | write!(f, "{} {} {} ", self.shade.0, self.shade.1, self.shade.2)?; 23 | write!(f, "{} {} {} ", self.theme.0, self.theme.1, self.theme.2)?; 24 | write!(f, "{} ", self.salt)?; 25 | write!(f, "{} {} #", self.deviation, self.distance) 26 | } 27 | } 28 | 29 | impl fmt::Display for SaltItem { 30 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 31 | write!(f, "{} {} {} ", self.color.0, self.color.1, self.color.2)?; 32 | write!(f, "{} {} ", self.likeliness, self.variability) 33 | } 34 | } 35 | 36 | impl fmt::Display for Salt { 37 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 38 | for item in self.0.iter() { 39 | write!(f, "{} ", item)?; 40 | } 41 | write!(f, "#") 42 | } 43 | } 44 | 45 | impl fmt::Display for Disc { 46 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 47 | write!( 48 | f, 49 | "Disc {} {} {} ", 50 | self.center.0, self.center.1, self.radius 51 | )?; 52 | write!(f, "{} #", self.color) 53 | } 54 | } 55 | 56 | impl fmt::Display for HalfPlane { 57 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 58 | write!( 59 | f, 60 | "HalfPlane {} {} {} {} ", 61 | self.limit.0, self.limit.1, self.reference.0, self.reference.1 62 | )?; 63 | write!(f, "{} #", self.color) 64 | } 65 | } 66 | 67 | impl fmt::Display for Triangle { 68 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 69 | write!(f, "Triangle {} {} ", self.a.0, self.a.1)?; 70 | write!(f, "{} {} ", self.b.0, self.b.1)?; 71 | write!(f, "{} {} ", self.c.0, self.c.1)?; 72 | write!(f, "{} #", self.color) 73 | } 74 | } 75 | 76 | impl fmt::Display for Spiral { 77 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 78 | write!( 79 | f, 80 | "Spiral {} {} {} {} ", 81 | self.center.0, self.center.1, self.width, self.tightness 82 | )?; 83 | write!(f, "{} #", self.color) 84 | } 85 | } 86 | 87 | impl fmt::Display for Stripe { 88 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 89 | write!( 90 | f, 91 | "Stripe {} {} {} {} ", 92 | self.limit.0, self.limit.1, self.reference.0, self.reference.1 93 | )?; 94 | write!(f, "{} #", self.color) 95 | } 96 | } 97 | 98 | impl fmt::Display for Wave { 99 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 100 | write!( 101 | f, 102 | "Wave {} {} {} {} ", 103 | self.limit.0, self.limit.1, self.reference.0, self.reference.1 104 | )?; 105 | write!(f, "{} {} ", self.amplitude, self.frequency)?; 106 | write!(f, "{} #", self.color) 107 | } 108 | } 109 | 110 | impl fmt::Display for Sawtooth { 111 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 112 | write!( 113 | f, 114 | "Sawtooth {} {} {} {} ", 115 | self.limit.0, self.limit.1, self.reference.0, self.reference.1 116 | )?; 117 | write!(f, "{} {} ", self.amplitude, self.frequency)?; 118 | write!(f, "{} #", self.color) 119 | } 120 | } 121 | 122 | impl fmt::Display for Logger { 123 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 124 | write!(f, "{} ", self.frame)?; 125 | write!(f, "{} ", self.bg)?; 126 | write!(f, "{} ", self.objects.len())?; 127 | for o in &self.objects { 128 | write!(f, "{} ", o)?; 129 | } 130 | Ok(()) 131 | } 132 | } 133 | 134 | impl Logger { 135 | pub fn save(&self, dest: &str) -> std::io::Result<()> { 136 | let mut buffer = File::create(dest)?; 137 | buffer.write_all(&format!("{}", &self).into_bytes()) 138 | } 139 | 140 | pub fn load(src: &str) -> Self { 141 | let mut s = String::new(); 142 | let mut file = File::open(src).unwrap(); 143 | file.read_to_string(&mut s).unwrap(); 144 | let mut items = s.split(' '); 145 | Logger::restore(&mut items) 146 | } 147 | } 148 | 149 | trait Restore { 150 | fn restore<'a>(items: &mut impl Iterator) -> Self; 151 | } 152 | 153 | impl Restore for usize { 154 | fn restore<'a>(items: &mut impl Iterator) -> Self { 155 | items.next().unwrap().parse::().unwrap() 156 | } 157 | } 158 | 159 | impl Restore for f64 { 160 | fn restore<'a>(items: &mut impl Iterator) -> Self { 161 | items.next().unwrap().parse::().unwrap() 162 | } 163 | } 164 | 165 | impl Restore for Pos { 166 | fn restore<'a>(items: &mut impl Iterator) -> Self { 167 | Self(f64::restore(items), f64::restore(items)) 168 | } 169 | } 170 | 171 | impl Restore for Color { 172 | fn restore<'a>(items: &mut impl Iterator) -> Self { 173 | Self( 174 | usize::restore(items), 175 | usize::restore(items), 176 | usize::restore(items), 177 | ) 178 | } 179 | } 180 | 181 | impl Restore for Logger { 182 | fn restore<'a>(items: &mut impl Iterator) -> Self { 183 | let frame = Frame::restore(items); 184 | let bg = ColorItem::restore(items); 185 | let len = items.next().unwrap().parse::().unwrap(); 186 | let mut objects = Vec::new(); 187 | for _ in 0..len { 188 | objects.push(match items.next().unwrap() { 189 | "Disc" => Rc::new(Disc::restore(items)) as Rc, 190 | "HalfPlane" => Rc::new(HalfPlane::restore(items)) as Rc, 191 | "Stripe" => Rc::new(Stripe::restore(items)) as Rc, 192 | "Triangle" => Rc::new(Triangle::restore(items)) as Rc, 193 | "Spiral" => Rc::new(Spiral::restore(items)) as Rc, 194 | "Wave" => Rc::new(Wave::restore(items)) as Rc, 195 | "Sawtooth" => Rc::new(Sawtooth::restore(items)) as Rc, 196 | _ => panic!("Unknown item"), 197 | }); 198 | } 199 | Self { frame, bg, objects } 200 | } 201 | } 202 | 203 | impl Restore for Frame { 204 | fn restore<'a>(items: &mut impl Iterator) -> Self { 205 | let x = usize::restore(items); 206 | let y = usize::restore(items); 207 | let w = usize::restore(items); 208 | let h = usize::restore(items); 209 | assert_eq!(items.next().unwrap(), "#"); 210 | Self { x, y, w, h } 211 | } 212 | } 213 | 214 | impl Restore for Salt { 215 | fn restore<'a>(items: &mut impl Iterator) -> Self { 216 | let nb = usize::restore(items); 217 | let mut salt = Salt::default(); 218 | for _ in 0..nb { 219 | salt.0.push(SaltItem::restore(items)); 220 | } 221 | assert_eq!(items.next().unwrap(), "#"); 222 | salt 223 | } 224 | } 225 | 226 | impl Restore for SaltItem { 227 | fn restore<'a>(items: &mut impl Iterator) -> Self { 228 | let color = Color::restore(items); 229 | let likeliness = f64::restore(items); 230 | let variability = usize::restore(items); 231 | Self { 232 | color, 233 | likeliness, 234 | variability, 235 | } 236 | } 237 | } 238 | 239 | impl Restore for ColorItem { 240 | fn restore<'a>(items: &mut impl Iterator) -> Self { 241 | let shade = Color::restore(items); 242 | let theme = Color::restore(items); 243 | let salt = Salt::restore(items); 244 | let deviation = usize::restore(items); 245 | let distance = usize::restore(items); 246 | assert_eq!(items.next().unwrap(), "#"); 247 | Self { 248 | shade, 249 | theme, 250 | deviation, 251 | distance, 252 | salt, 253 | } 254 | } 255 | } 256 | 257 | impl Restore for Disc { 258 | fn restore<'a>(items: &mut impl Iterator) -> Self { 259 | let center = Pos::restore(items); 260 | let radius = f64::restore(items); 261 | let color = ColorItem::restore(items); 262 | assert_eq!(items.next().unwrap(), "#"); 263 | Self { 264 | center, 265 | radius, 266 | color, 267 | } 268 | } 269 | } 270 | 271 | impl Restore for HalfPlane { 272 | fn restore<'a>(items: &mut impl Iterator) -> Self { 273 | let limit = Pos::restore(items); 274 | let reference = Pos::restore(items); 275 | let color = ColorItem::restore(items); 276 | assert_eq!(items.next().unwrap(), "#"); 277 | Self { 278 | limit, 279 | reference, 280 | color, 281 | } 282 | } 283 | } 284 | 285 | impl Restore for Stripe { 286 | fn restore<'a>(items: &mut impl Iterator) -> Self { 287 | let limit = Pos::restore(items); 288 | let reference = Pos::restore(items); 289 | let color = ColorItem::restore(items); 290 | assert_eq!(items.next().unwrap(), "#"); 291 | Self { 292 | limit, 293 | reference, 294 | color, 295 | } 296 | } 297 | } 298 | 299 | impl Restore for Triangle { 300 | fn restore<'a>(items: &mut impl Iterator) -> Self { 301 | let a = Pos::restore(items); 302 | let b = Pos::restore(items); 303 | let c = Pos::restore(items); 304 | let color = ColorItem::restore(items); 305 | assert_eq!(items.next().unwrap(), "#"); 306 | Self { a, b, c, color } 307 | } 308 | } 309 | 310 | impl Restore for Spiral { 311 | fn restore<'a>(items: &mut impl Iterator) -> Self { 312 | let center = Pos::restore(items); 313 | let width = f64::restore(items); 314 | let tightness = f64::restore(items); 315 | let color = ColorItem::restore(items); 316 | assert_eq!(items.next().unwrap(), "#"); 317 | Self { 318 | center, 319 | width, 320 | color, 321 | tightness, 322 | } 323 | } 324 | } 325 | 326 | impl Restore for Wave { 327 | fn restore<'a>(items: &mut impl Iterator) -> Self { 328 | let limit = Pos::restore(items); 329 | let reference = Pos::restore(items); 330 | let amplitude = f64::restore(items); 331 | let frequency = f64::restore(items); 332 | let color = ColorItem::restore(items); 333 | assert_eq!(items.next().unwrap(), "#"); 334 | Self { 335 | limit, 336 | reference, 337 | amplitude, 338 | frequency, 339 | color, 340 | } 341 | } 342 | } 343 | 344 | impl Restore for Sawtooth { 345 | fn restore<'a>(items: &mut impl Iterator) -> Self { 346 | let limit = Pos::restore(items); 347 | let reference = Pos::restore(items); 348 | let amplitude = f64::restore(items); 349 | let frequency = f64::restore(items); 350 | let color = ColorItem::restore(items); 351 | assert_eq!(items.next().unwrap(), "#"); 352 | Self { 353 | limit, 354 | reference, 355 | amplitude, 356 | frequency, 357 | color, 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /setup/wallrnd.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | deviation = 15 # How much the color varies within a shape. 0 for uniform shapes. 3 | weight = 25 # How far from the theme are the colors. 0 for theme-only colors, 100 for fully random. 4 | size = 30.0 # How big are tiles. Can be overriden for each tiling 5 | # You should set the dimensions TWICE AS BIG as you actual screen 6 | # resolution if you're going to generate SVG with line width 0.0 7 | # This will truly hide the lines. 8 | # Warning: massive performance hit when converting to PNG, SVG is still fine 9 | # ("massive" being ~700ms instead of ~130ms in --release) 10 | width = 2732 # Screen dimension 11 | height = 1536 # Screen dimension 12 | 13 | [lines] 14 | # Set line appearance 15 | width = 0.0 16 | color = "#000000" # Supports named colors as well 17 | # Tiling-specific overrides 18 | # Available: 19 | # del_width, del_color, rho_width, rho_color, 20 | # hex_width, hex_color, tri_width, tri_color, 21 | # hex_and_tri_width, hex_and_tri_color, 22 | # squ_and_tri_width, squ_and_tri_color, 23 | # pen_width, pen_color, 24 | 25 | [colors] 26 | # List of named colors 27 | # Either [r, g, b] (dec.) or #RRGGBB (hex.), both 0-255 28 | # List of colors here: all SVG named colors 29 | black = "#000000" 30 | navy = "#000080" 31 | darkblue = "#00008B" 32 | mediumblue = "#0000CD" 33 | blue = "#0000FF" 34 | darkgreen = "#006400" 35 | green = "#008000" 36 | teal = "#008080" 37 | darkcyan = "#008B8B" 38 | deepskyblue = "#00BFFF" 39 | darkturquoise = "#00CED1" 40 | mediumspringgreen = "#00FA9A" 41 | lime = "#00FF00" 42 | springgreen = "#00FF7F" 43 | cyan = "#00FFFF" 44 | aqua = "#00FFFF" 45 | midnightblue = "#191970" 46 | dodgerblue = "#1E90FF" 47 | lightseagreen = "#20B2AA" 48 | forestgreen = "#228B22" 49 | seagreen = "#2E8B57" 50 | darkslategray = "#2F4F4F" 51 | darkslategrey = "#2F4F4F" 52 | limegreen = "#32CD32" 53 | mediumseagreen = "#3CB371" 54 | turquoise = "#40E0D0" 55 | royalblue = "#4169E1" 56 | steelblue = "#4682B4" 57 | darkslateblue = "#483D8B" 58 | mediumturquoise = "#48D1CC" 59 | indigo = "#4B0082" 60 | darkolivegreen = "#556B2F" 61 | cadetblue = "#5F9EA0" 62 | cornflowerblue = "#6495ED" 63 | mediumaquamarine = "#66CDAA" 64 | dimgrey = "#696969" 65 | dimgray = "#696969" 66 | slateblue = "#6A5ACD" 67 | olivedrab = "#6B8E23" 68 | slategrey = "#708090" 69 | slategray = "#708090" 70 | lightslategray = "#778899" 71 | lightslategrey = "#778899" 72 | mediumslateblue = "#7B68EE" 73 | lawngreen = "#7CFC00" 74 | chartreuse = "#7FFF00" 75 | aquamarine = "#7FFFD4" 76 | maroon = "#800000" 77 | purple = "#800080" 78 | olive = "#808000" 79 | gray = "#808080" 80 | grey = "#808080" 81 | skyblue = "#87CEEB" 82 | lightskyblue = "#87CEFA" 83 | blueviolet = "#8A2BE2" 84 | darkred = "#8B0000" 85 | darkmagenta = "#8B008B" 86 | saddlebrown = "#8B4513" 87 | darkseagreen = "#8FBC8F" 88 | lightgreen = "#90EE90" 89 | mediumpurple = "#9370DB" 90 | darkviolet = "#9400D3" 91 | palegreen = "#98FB98" 92 | darkorchid = "#9932CC" 93 | yellowgreen = "#9ACD32" 94 | sienna = "#A0522D" 95 | brown = "#A52A2A" 96 | darkgray = "#A9A9A9" 97 | darkgrey = "#A9A9A9" 98 | lightblue = "#ADD8E6" 99 | greenyellow = "#ADFF2F" 100 | paleturquoise = "#AFEEEE" 101 | lightsteelblue = "#B0C4DE" 102 | powderblue = "#B0E0E6" 103 | firebrick = "#B22222" 104 | darkgoldenrod = "#B8860B" 105 | mediumorchid = "#BA55D3" 106 | rosybrown = "#BC8F8F" 107 | darkkhaki = "#BDB76B" 108 | silver = "#C0C0C0" 109 | mediumvioletred = "#C71585" 110 | indianred = "#CD5C5C" 111 | peru = "#CD853F" 112 | chocolate = "#D2691E" 113 | tan = "#D2B48C" 114 | lightgray = "#D3D3D3" 115 | lightgrey = "#D3D3D3" 116 | thistle = "#D8BFD8" 117 | orchid = "#DA70D6" 118 | goldenrod = "#DAA520" 119 | palevioletred = "#DB7093" 120 | crimson = "#DC143C" 121 | gainsboro = "#DCDCDC" 122 | plum = "#DDA0DD" 123 | burlywood = "#DEB887" 124 | lightcyan = "#E0FFFF" 125 | lavender = "#E6E6FA" 126 | darksalmon = "#E9967A" 127 | violet = "#EE82EE" 128 | palegoldenrod = "#EEE8AA" 129 | lightcoral = "#F08080" 130 | khaki = "#F0E68C" 131 | aliceblue = "#F0F8FF" 132 | honeydew = "#F0FFF0" 133 | azure = "#F0FFFF" 134 | sandybrown = "#F4A460" 135 | wheat = "#F5DEB3" 136 | beige = "#F5F5DC" 137 | whitesmoke = "#F5F5F5" 138 | mintcream = "#F5FFFA" 139 | ghostwhite = "#F8F8FF" 140 | salmon = "#FA8072" 141 | antiquewhite = "#FAEBD7" 142 | linen = "#FAF0E6" 143 | lightgoldenrodyellow = "#FAFAD2" 144 | oldlace = "#FDF5E6" 145 | red = "#FF0000" 146 | fuchsia = "#FF00FF" 147 | magenta = "#FF00FF" 148 | deeppink = "#FF1493" 149 | orangered = "#FF4500" 150 | tomato = "#FF6347" 151 | hotpink = "#FF69B4" 152 | coral = "#FF7F50" 153 | darkorange = "#FF8C00" 154 | lightsalmon = "#FFA07A" 155 | orange = "#FFA500" 156 | lightpink = "#FFB6C1" 157 | pink = "#FFC0CB" 158 | gold = "#FFD700" 159 | peachpuff = "#FFDAB9" 160 | navajowhite = "#FFDEAD" 161 | moccasin = "#FFE4B5" 162 | bisque = "#FFE4C4" 163 | mistyrose = "#FFE4E1" 164 | blanchedalmond = "#FFEBCD" 165 | papayawhip = "#FFEFD5" 166 | lavenderblush = "#FFF0F5" 167 | seashell = "#FFF5EE" 168 | cornsilk = "#FFF8DC" 169 | lemonchiffon = "#FFFACD" 170 | floralwhite = "#FFFAF0" 171 | snow = "#FFFAFA" 172 | yellow = "#FFFF00" 173 | lightyellow = "#FFFFE0" 174 | ivory = "#FFFFF0" 175 | white = "#FFFFFF" 176 | 177 | [themes] 178 | # A theme is a list of weighted colors (named or not); e.g. theme1 = ["white x20 ~10", "black x10"] 179 | # You may include a previously created theme inside another; e.g. theme2 = ["theme2", "grey x20"] 180 | # A color can also be described by a table: { color, weight, distance, variability } 181 | # One can add a 'salt' field, described by an array of tables { color, likeliness, variability } 182 | # 'color' is any named color or [R, G, B] or #RRGGBB 183 | # 'likeliness' is the probability to choose a certain salt (recommended ~1%) 184 | # 'variability' is how much deviation there is in the salt 185 | # In summary, here is the recomended format: 186 | # = [ 187 | # { color = , weight = , distance = , variability = , salt = [ 188 | # { color = , likeliness = , variability = }, 189 | # ... 190 | # { color = , likeliness = , variability = }] }, 191 | # ... 192 | # { color = , weight = , distance = , variability = , salt = [ 193 | # { color = , likeliness = , variability = }, 194 | # ... 195 | # { color = , likeliness = , variability = }] }, 196 | night = [ 197 | { color = "#191970", weight = 40, distance = 20 }, 198 | { color = "#000000", weight = 30, distance = 5, variability = 10, salt = [ 199 | { color = "#CCCCCC", likeliness = 0.03, variability = 20 }] }, 200 | { color = "#708090", weight = 10, distance = 10, variability = 5, salt = [ 201 | { color = "#445566", likeliness = 0.02, variability = 30 }] } 202 | ] 203 | dawn = [ 204 | { color = "#FF00FF", weight = 40, variability = 50, salt = [ 205 | { color = "#DAA520", likeliness = 0.03, variability = 10 }] }, 206 | { color = "#00FFFF", weight = 30 }, 207 | { color = "#DAA520", weight = 20, distance = 10, salt = [ 208 | { color = "#FF00FF", likeliness = 0.03, variability = 10 }] } 209 | ] 210 | spring = [ 211 | { color = "#7CFC00", weight = 30, variability = 60, salt = [ 212 | { color = "#108000", likeliness = 0.03, variability = 20 }, 213 | { color = "#CCFF00", likeliness = 0.04, variability = 20 }] }, 214 | { color = "#FFD700", weight = 10, salt = [ 215 | { color = "#108000", likeliness = 0.03, variability = 20 }, 216 | { color = "#CCFF00", likeliness = 0.04, variability = 20 }] }, 217 | { color = "#00FFFF", weight = 10 , salt = [ 218 | { color = "#0022CC", likeliness = 0.007, variability = 50 }] }, 219 | { color = "#FF00FF", weight = 10, distance = 10, variability = 40 } 220 | ] 221 | sky = [ 222 | { color = "#00FFFF", weight = 50, salt = [ 223 | { color = "#0022CC", likeliness = 0.01, variability = 50 }] }, 224 | { color = "#87CEEB", weight = 50, salt = [ 225 | { color = "#CCCCCC", likeliness = 0.08, variability = 20 }] }, 226 | { color = "#FFD700", weight = 30, variability = 50, salt = [ 227 | { color = "#990000", likeliness = 0.02, variability = 20 }] } 228 | ] 229 | rust = [ 230 | { color = "#FF7F50", weight = 40, distance = 20, salt = [ 231 | { color = "#BBBBBB", likeliness = 0.02, variability = 30 }] }, 232 | { color = "#FFA500", weight = 30, distance = 20 }, 233 | { color = "#C0C0C0", weight = 20, distance = 10, variability = 10, salt = [ 234 | { color = "#00DD88", likeliness = 0.01, variability = 20 }, 235 | { color = "#FFA500", likeliness = 0.02, variability = 10 }] }, 236 | { color = "#00FF7F", weight = 10, distance = 0, variability = 50 } 237 | ] 238 | fire = [ 239 | { color = "#FF4500", weight = 30, variability = 30 , salt = [ 240 | { color = "#990000", likeliness = 0.02, variability = 30 }] }, 241 | { color = "#FFFF00", weight = 20, distance = 10, salt = [ 242 | { color = "#AAAA00", likeliness = 0.01, variability = 20 }] }, 243 | { color = "#FF0000", weight = 20, variability = 30 }, 244 | { color = "#8B0000", weight = 10 } 245 | ] 246 | forest = [ 247 | { color = "#228B22", weight = 40, variability = 30, salt = [ 248 | { color = "#FFD700", likeliness = 0.005, variability = 10 }, 249 | { color = "#FF00AA", likeliness = 0.005, variability = 10 }] }, 250 | { color = "#9ACD32", weight = 20, variability = 30, distance = 10 }, 251 | { color = "#8B4513", weight = 20, distance = 10, salt = [ 252 | { color = "#BE7846", likeliness = 0.02, variability = 10 }] } 253 | ] 254 | ocean = [ 255 | { color = "#000080", weight = 60, salt = [ 256 | { color = "#111111", likeliness = 0.02, variability = 20 }] }, 257 | { color = "#4B0082", weight = 30, salt = [ 258 | { color = "#8F44C6", likeliness = 0.01, variability = 10 }] }, 259 | { color = "#66CDAA", weight = 30 } 260 | ] 261 | blood = [ 262 | { color = "#800000", weight = 30, salt = [ 263 | { color = "#220000", likeliness = 0.03, variability = 20 }, 264 | { color = "#CC0011", likeliness = 0.02, variability = 10 }] }, 265 | { color = "#FA8072", weight = 20 }, 266 | { color = "#DC143C", weight = 10 } 267 | ] 268 | dusk = [ 269 | { color = "#7B68EE", weight = 30, salt = [ 270 | { color = "#CC00CC", likeliness = 0.02, variability = 10 }] }, 271 | { color = "#FF69B4", weight = 20, variability = 30 }, 272 | { color = "#FF8C00", weight = 20, variability = 30, salt = [ 273 | { color = "#BB5500", likeliness = 0.01, variability = 20 }] } 274 | ] 275 | shadow = [ 276 | { color = "#4B0082", weight = 30 }, 277 | { color = "#800080", weight = 20, variability = 30, distance = 5, salt = [ 278 | { color = "#200030", likeliness = 0.02, variability = 5 }] }, 279 | { color = "#000000", weight = 10, variability = 5, distance = 5, salt = [ 280 | { color = "#500050", likeliness = 0.02, variability = 10 }] } 281 | ] 282 | 283 | [shapes] 284 | # Both tilings and patterns 285 | # Empty list means all available 286 | # Examples: 287 | # cstr_or_ccir_and_any_tiling = ["crossed-stripes", "concentric-circles"] 288 | # ftri_and_del = ["free-triangle", "delaunay"] 289 | # all = [] 290 | # 291 | # All names: 292 | # Tilings 293 | # "H" / "hex." / "hexagons" 294 | # "T" / "tri." / "triangles" 295 | # "H&T" / "hex.&tri." / "hexagons&squares" 296 | # "S&T" / "squ.&tri." / "squares&triangles" 297 | # "R" / "rho." / "rhombus" 298 | # "D" / "del." / "delaunay" 299 | # "P" / "pen." / "pentagons" 300 | # Patterns 301 | # "FC" / "f-cir." / "free-circles" 302 | # "FT" / "f-tri." / "free-triangles" 303 | # "FR" / "f-str." / "free-stripes" 304 | # "FP" / "f-spi." / "free-spirals" 305 | # "CC" / "c-cir." / "concentric-circles" 306 | # "PS" / "p-str." / "parallel-stripes" 307 | # "CS" / "c-str." / "crossed-stripes" 308 | # "PW" / "p-wav." / "parallel-waves" 309 | # "PT" / "p-saw." / "parallel-sawteeth" 310 | all = [] 311 | 312 | [data.patterns] 313 | nb_free_triangles = 15 314 | nb_free_circles = 15 315 | nb_free_stripes = 15 316 | nb_parallel_stripes = 15 317 | nb_concentric_circles = 13 318 | nb_crossed_stripes = 13 319 | nb_free_spirals = 3 320 | nb_parallel_waves = 10 321 | nb_parallel_sawteeth = 10 322 | var_parallel_stripes = 10 # Variability of the orientation 323 | var_crossed_stripes = 10 324 | width_spiral = 0.2 # As a proportion of the window height 325 | width_stripe = 0.2 326 | width_wave = 0.7 327 | width_sawtooth = 0.5 328 | tightness_spiral = 0.3 # As a proportion of width_spiral 329 | 330 | [data.tilings] 331 | size_hex = 10.0 # In absolute size 332 | size_tri = 10.0 333 | size_hex_and_tri = 10.0 334 | size_squ_and_tri = 10.0 335 | size_rho = 14.0 336 | size_pen = 14.0 337 | nb_delaunay = 20000 338 | 339 | [[entry]] 340 | themes = ["night"] # List of possible themes linked to this entry 341 | span = "0000-0559" # Time frame of availability for this entry as "HHMM-HHMM" (begin-end) 342 | shapes = ["all"] # Named shape configuration 343 | # Not used here: 344 | # `weight = W` 345 | # gives certain entries different ponderations when several of them overlap on a single time frame. 346 | # `line_color = C` 347 | # override line settings 348 | 349 | [[entry]] 350 | themes = ["dawn"] 351 | span = "0600-0729" 352 | shapes = ["all"] 353 | 354 | [[entry]] 355 | themes = ["spring"] 356 | span = "0730-0859" 357 | shapes = ["all"] 358 | 359 | [[entry]] 360 | themes = ["sky"] 361 | span = "0900-1029" 362 | shapes = ["all"] 363 | 364 | [[entry]] 365 | themes = ["rust"] 366 | span = "1030-1159" 367 | shapes = ["all"] 368 | 369 | [[entry]] 370 | themes = ["fire"] 371 | span = "1200-1329" 372 | shapes = ["all"] 373 | 374 | [[entry]] 375 | themes = ["forest"] 376 | span = "1330-1459" 377 | shapes = ["all"] 378 | 379 | [[entry]] 380 | themes = ["ocean"] 381 | span = "1500-1629" 382 | shapes = ["all"] 383 | 384 | [[entry]] 385 | themes = ["blood"] 386 | span = "1630-1759" 387 | shapes = ["all"] 388 | 389 | [[entry]] 390 | themes = ["dusk"] 391 | span = "1800-1929" 392 | shapes = ["all"] 393 | 394 | [[entry]] 395 | themes = ["shadow"] 396 | span = "1930-2059" 397 | shapes = ["all"] 398 | 399 | [[entry]] 400 | themes = ["night"] 401 | span = "2100-2359" 402 | shapes = ["all"] 403 | -------------------------------------------------------------------------------- /assets/default.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | deviation = 15 # How much the color varies within a shape. 0 for uniform shapes. 3 | weight = 25 # How far from the theme are the colors. 0 for theme-only colors, 100 for fully random. 4 | size = 30.0 # How big are tiles. Can be overriden for each tiling 5 | # You should set the dimensions TWICE AS BIG as you actual screen 6 | # resolution if you're going to generate SVG with line width 0.0 7 | # This will truly hide the lines. 8 | # Warning: massive performance hit when converting to PNG, SVG is still fine 9 | # ("massive" being ~700ms instead of ~130ms in --release) 10 | width = 2732 # Screen dimension 11 | height = 1536 # Screen dimension 12 | 13 | [lines] 14 | # Set line appearance 15 | width = 0.0 16 | color = "#000000" # Supports named colors as well 17 | # Tiling-specific overrides 18 | # Available: 19 | # del_width, del_color, rho_width, rho_color, 20 | # hex_width, hex_color, tri_width, tri_color, 21 | # hex_and_tri_width, hex_and_tri_color, 22 | # squ_and_tri_width, squ_and_tri_color, 23 | # pen_width, pen_color, 24 | 25 | [colors] 26 | # List of named colors 27 | # Either [r, g, b] (dec.) or #RRGGBB (hex.), both 0-255 28 | # List of colors here: all SVG named colors 29 | black = "#000000" 30 | navy = "#000080" 31 | darkblue = "#00008B" 32 | mediumblue = "#0000CD" 33 | blue = "#0000FF" 34 | darkgreen = "#006400" 35 | green = "#008000" 36 | teal = "#008080" 37 | darkcyan = "#008B8B" 38 | deepskyblue = "#00BFFF" 39 | darkturquoise = "#00CED1" 40 | mediumspringgreen = "#00FA9A" 41 | lime = "#00FF00" 42 | springgreen = "#00FF7F" 43 | cyan = "#00FFFF" 44 | aqua = "#00FFFF" 45 | midnightblue = "#191970" 46 | dodgerblue = "#1E90FF" 47 | lightseagreen = "#20B2AA" 48 | forestgreen = "#228B22" 49 | seagreen = "#2E8B57" 50 | darkslategray = "#2F4F4F" 51 | darkslategrey = "#2F4F4F" 52 | limegreen = "#32CD32" 53 | mediumseagreen = "#3CB371" 54 | turquoise = "#40E0D0" 55 | royalblue = "#4169E1" 56 | steelblue = "#4682B4" 57 | darkslateblue = "#483D8B" 58 | mediumturquoise = "#48D1CC" 59 | indigo = "#4B0082" 60 | darkolivegreen = "#556B2F" 61 | cadetblue = "#5F9EA0" 62 | cornflowerblue = "#6495ED" 63 | mediumaquamarine = "#66CDAA" 64 | dimgrey = "#696969" 65 | dimgray = "#696969" 66 | slateblue = "#6A5ACD" 67 | olivedrab = "#6B8E23" 68 | slategrey = "#708090" 69 | slategray = "#708090" 70 | lightslategray = "#778899" 71 | lightslategrey = "#778899" 72 | mediumslateblue = "#7B68EE" 73 | lawngreen = "#7CFC00" 74 | chartreuse = "#7FFF00" 75 | aquamarine = "#7FFFD4" 76 | maroon = "#800000" 77 | purple = "#800080" 78 | olive = "#808000" 79 | gray = "#808080" 80 | grey = "#808080" 81 | skyblue = "#87CEEB" 82 | lightskyblue = "#87CEFA" 83 | blueviolet = "#8A2BE2" 84 | darkred = "#8B0000" 85 | darkmagenta = "#8B008B" 86 | saddlebrown = "#8B4513" 87 | darkseagreen = "#8FBC8F" 88 | lightgreen = "#90EE90" 89 | mediumpurple = "#9370DB" 90 | darkviolet = "#9400D3" 91 | palegreen = "#98FB98" 92 | darkorchid = "#9932CC" 93 | yellowgreen = "#9ACD32" 94 | sienna = "#A0522D" 95 | brown = "#A52A2A" 96 | darkgray = "#A9A9A9" 97 | darkgrey = "#A9A9A9" 98 | lightblue = "#ADD8E6" 99 | greenyellow = "#ADFF2F" 100 | paleturquoise = "#AFEEEE" 101 | lightsteelblue = "#B0C4DE" 102 | powderblue = "#B0E0E6" 103 | firebrick = "#B22222" 104 | darkgoldenrod = "#B8860B" 105 | mediumorchid = "#BA55D3" 106 | rosybrown = "#BC8F8F" 107 | darkkhaki = "#BDB76B" 108 | silver = "#C0C0C0" 109 | mediumvioletred = "#C71585" 110 | indianred = "#CD5C5C" 111 | peru = "#CD853F" 112 | chocolate = "#D2691E" 113 | tan = "#D2B48C" 114 | lightgray = "#D3D3D3" 115 | lightgrey = "#D3D3D3" 116 | thistle = "#D8BFD8" 117 | orchid = "#DA70D6" 118 | goldenrod = "#DAA520" 119 | palevioletred = "#DB7093" 120 | crimson = "#DC143C" 121 | gainsboro = "#DCDCDC" 122 | plum = "#DDA0DD" 123 | burlywood = "#DEB887" 124 | lightcyan = "#E0FFFF" 125 | lavender = "#E6E6FA" 126 | darksalmon = "#E9967A" 127 | violet = "#EE82EE" 128 | palegoldenrod = "#EEE8AA" 129 | lightcoral = "#F08080" 130 | khaki = "#F0E68C" 131 | aliceblue = "#F0F8FF" 132 | honeydew = "#F0FFF0" 133 | azure = "#F0FFFF" 134 | sandybrown = "#F4A460" 135 | wheat = "#F5DEB3" 136 | beige = "#F5F5DC" 137 | whitesmoke = "#F5F5F5" 138 | mintcream = "#F5FFFA" 139 | ghostwhite = "#F8F8FF" 140 | salmon = "#FA8072" 141 | antiquewhite = "#FAEBD7" 142 | linen = "#FAF0E6" 143 | lightgoldenrodyellow = "#FAFAD2" 144 | oldlace = "#FDF5E6" 145 | red = "#FF0000" 146 | fuchsia = "#FF00FF" 147 | magenta = "#FF00FF" 148 | deeppink = "#FF1493" 149 | orangered = "#FF4500" 150 | tomato = "#FF6347" 151 | hotpink = "#FF69B4" 152 | coral = "#FF7F50" 153 | darkorange = "#FF8C00" 154 | lightsalmon = "#FFA07A" 155 | orange = "#FFA500" 156 | lightpink = "#FFB6C1" 157 | pink = "#FFC0CB" 158 | gold = "#FFD700" 159 | peachpuff = "#FFDAB9" 160 | navajowhite = "#FFDEAD" 161 | moccasin = "#FFE4B5" 162 | bisque = "#FFE4C4" 163 | mistyrose = "#FFE4E1" 164 | blanchedalmond = "#FFEBCD" 165 | papayawhip = "#FFEFD5" 166 | lavenderblush = "#FFF0F5" 167 | seashell = "#FFF5EE" 168 | cornsilk = "#FFF8DC" 169 | lemonchiffon = "#FFFACD" 170 | floralwhite = "#FFFAF0" 171 | snow = "#FFFAFA" 172 | yellow = "#FFFF00" 173 | lightyellow = "#FFFFE0" 174 | ivory = "#FFFFF0" 175 | white = "#FFFFFF" 176 | 177 | [themes] 178 | # A theme is a list of weighted colors (named or not); e.g. theme1 = ["white x20 ~10", "black x10"] 179 | # You may include a previously created theme inside another; e.g. theme2 = ["theme2", "grey x20"] 180 | # A color can also be described by a table: { color, weight, distance, variability } 181 | # One can add a 'salt' field, described by an array of tables { color, likeliness, variability } 182 | # 'color' is any named color or [R, G, B] or #RRGGBB 183 | # 'likeliness' is the probability to choose a certain salt (recommended ~1%) 184 | # 'variability' is how much deviation there is in the salt 185 | # In summary, here is the recomended format: 186 | # = [ 187 | # { color = , weight = , distance = , variability = , salt = [ 188 | # { color = , likeliness = , variability = }, 189 | # ... 190 | # { color = , likeliness = , variability = }] }, 191 | # ... 192 | # { color = , weight = , distance = , variability = , salt = [ 193 | # { color = , likeliness = , variability = }, 194 | # ... 195 | # { color = , likeliness = , variability = }] }, 196 | night = [ 197 | { color = "#191970", weight = 40, distance = 20 }, 198 | { color = "#000000", weight = 30, distance = 5, variability = 10, salt = [ 199 | { color = "#CCCCCC", likeliness = 0.03, variability = 20 }] }, 200 | { color = "#708090", weight = 10, distance = 10, variability = 5, salt = [ 201 | { color = "#445566", likeliness = 0.02, variability = 30 }] } 202 | ] 203 | dawn = [ 204 | { color = "#FF00FF", weight = 40, variability = 50, salt = [ 205 | { color = "#DAA520", likeliness = 0.03, variability = 10 }] }, 206 | { color = "#00FFFF", weight = 30 }, 207 | { color = "#DAA520", weight = 20, distance = 10, salt = [ 208 | { color = "#FF00FF", likeliness = 0.03, variability = 10 }] } 209 | ] 210 | spring = [ 211 | { color = "#7CFC00", weight = 30, variability = 60, salt = [ 212 | { color = "#108000", likeliness = 0.03, variability = 20 }, 213 | { color = "#CCFF00", likeliness = 0.04, variability = 20 }] }, 214 | { color = "#FFD700", weight = 10, salt = [ 215 | { color = "#108000", likeliness = 0.03, variability = 20 }, 216 | { color = "#CCFF00", likeliness = 0.04, variability = 20 }] }, 217 | { color = "#00FFFF", weight = 10 , salt = [ 218 | { color = "#0022CC", likeliness = 0.007, variability = 50 }] }, 219 | { color = "#FF00FF", weight = 10, distance = 10, variability = 40 } 220 | ] 221 | sky = [ 222 | { color = "#00FFFF", weight = 50, salt = [ 223 | { color = "#0022CC", likeliness = 0.01, variability = 50 }] }, 224 | { color = "#87CEEB", weight = 50, salt = [ 225 | { color = "#CCCCCC", likeliness = 0.08, variability = 20 }] }, 226 | { color = "#FFD700", weight = 30, variability = 50, salt = [ 227 | { color = "#990000", likeliness = 0.02, variability = 20 }] } 228 | ] 229 | rust = [ 230 | { color = "#FF7F50", weight = 40, distance = 20, salt = [ 231 | { color = "#BBBBBB", likeliness = 0.02, variability = 30 }] }, 232 | { color = "#FFA500", weight = 30, distance = 20 }, 233 | { color = "#C0C0C0", weight = 20, distance = 10, variability = 10, salt = [ 234 | { color = "#00DD88", likeliness = 0.01, variability = 20 }, 235 | { color = "#FFA500", likeliness = 0.02, variability = 10 }] }, 236 | { color = "#00FF7F", weight = 10, distance = 0, variability = 50 } 237 | ] 238 | fire = [ 239 | { color = "#FF4500", weight = 30, variability = 30 , salt = [ 240 | { color = "#990000", likeliness = 0.02, variability = 30 }] }, 241 | { color = "#FFFF00", weight = 20, distance = 10, salt = [ 242 | { color = "#AAAA00", likeliness = 0.01, variability = 20 }] }, 243 | { color = "#FF0000", weight = 20, variability = 30 }, 244 | { color = "#8B0000", weight = 10 } 245 | ] 246 | forest = [ 247 | { color = "#228B22", weight = 40, variability = 30, salt = [ 248 | { color = "#FFD700", likeliness = 0.005, variability = 10 }, 249 | { color = "#FF00AA", likeliness = 0.005, variability = 10 }] }, 250 | { color = "#9ACD32", weight = 20, variability = 30, distance = 10 }, 251 | { color = "#8B4513", weight = 20, distance = 10, salt = [ 252 | { color = "#BE7846", likeliness = 0.02, variability = 10 }] } 253 | ] 254 | ocean = [ 255 | { color = "#000080", weight = 60, salt = [ 256 | { color = "#111111", likeliness = 0.02, variability = 20 }] }, 257 | { color = "#4B0082", weight = 30, salt = [ 258 | { color = "#8F44C6", likeliness = 0.01, variability = 10 }] }, 259 | { color = "#66CDAA", weight = 30 } 260 | ] 261 | blood = [ 262 | { color = "#800000", weight = 30, salt = [ 263 | { color = "#220000", likeliness = 0.03, variability = 20 }, 264 | { color = "#CC0011", likeliness = 0.02, variability = 10 }] }, 265 | { color = "#FA8072", weight = 20 }, 266 | { color = "#DC143C", weight = 10 } 267 | ] 268 | dusk = [ 269 | { color = "#7B68EE", weight = 30, salt = [ 270 | { color = "#CC00CC", likeliness = 0.02, variability = 10 }] }, 271 | { color = "#FF69B4", weight = 20, variability = 30 }, 272 | { color = "#FF8C00", weight = 20, variability = 30, salt = [ 273 | { color = "#BB5500", likeliness = 0.01, variability = 20 }] } 274 | ] 275 | shadow = [ 276 | { color = "#4B0082", weight = 30 }, 277 | { color = "#800080", weight = 20, variability = 30, distance = 5, salt = [ 278 | { color = "#200030", likeliness = 0.02, variability = 5 }] }, 279 | { color = "#000000", weight = 10, variability = 5, distance = 5, salt = [ 280 | { color = "#500050", likeliness = 0.02, variability = 10 }] } 281 | ] 282 | 283 | [shapes] 284 | # Both tilings and patterns 285 | # Empty list means all available 286 | # Examples: 287 | # cstr_or_ccir_and_any_tiling = ["crossed-stripes", "concentric-circles"] 288 | # ftri_and_del = ["free-triangle", "delaunay"] 289 | # all = [] 290 | # 291 | # All names: 292 | # Tilings 293 | # "H" / "hex." / "hexagons" 294 | # "T" / "tri." / "triangles" 295 | # "H&T" / "hex.&tri." / "hexagons&squares" 296 | # "S&T" / "squ.&tri." / "squares&triangles" 297 | # "R" / "rho." / "rhombus" 298 | # "D" / "del." / "delaunay" 299 | # "P" / "pen." / "pentagons" 300 | # Patterns 301 | # "FC" / "f-cir." / "free-circles" 302 | # "FT" / "f-tri." / "free-triangles" 303 | # "FR" / "f-str." / "free-stripes" 304 | # "FP" / "f-spi." / "free-spirals" 305 | # "CC" / "c-cir." / "concentric-circles" 306 | # "PS" / "p-str." / "parallel-stripes" 307 | # "CS" / "c-str." / "crossed-stripes" 308 | # "PW" / "p-wav." / "parallel-waves" 309 | # "PT" / "p-saw." / "parallel-sawteeth" 310 | all = [] 311 | 312 | [data.patterns] 313 | nb_free_triangles = 15 314 | nb_free_circles = 15 315 | nb_free_stripes = 15 316 | nb_parallel_stripes = 15 317 | nb_concentric_circles = 13 318 | nb_crossed_stripes = 13 319 | nb_free_spirals = 3 320 | nb_parallel_waves = 10 321 | nb_parallel_sawteeth = 10 322 | var_parallel_stripes = 10 # Variability of the orientation 323 | var_crossed_stripes = 10 324 | width_spiral = 0.2 # As a proportion of the window height 325 | width_stripe = 0.2 326 | width_wave = 0.7 327 | width_sawtooth = 0.5 328 | tightness_spiral = 0.3 # As a proportion of width_spiral 329 | 330 | [data.tilings] 331 | size_hex = 10.0 # In absolute size 332 | size_tri = 10.0 333 | size_hex_and_tri = 10.0 334 | size_squ_and_tri = 10.0 335 | size_rho = 14.0 336 | size_pen = 14.0 337 | nb_delaunay = 20000 338 | 339 | [[entry]] 340 | themes = ["night"] # List of possible themes linked to this entry 341 | span = "0000-0559" # Time frame of availability for this entry as "HHMM-HHMM" (begin-end) 342 | shapes = ["all"] # Named shape configuration 343 | # Not used here: 344 | # `weight = W` 345 | # gives certain entries different ponderations when several of them overlap on a single time frame. 346 | # `line_color = C` 347 | # override line settings 348 | 349 | [[entry]] 350 | themes = ["dawn"] 351 | span = "0600-0729" 352 | shapes = ["all"] 353 | 354 | [[entry]] 355 | themes = ["spring"] 356 | span = "0730-0859" 357 | shapes = ["all"] 358 | 359 | [[entry]] 360 | themes = ["sky"] 361 | span = "0900-1029" 362 | shapes = ["all"] 363 | 364 | [[entry]] 365 | themes = ["rust"] 366 | span = "1030-1159" 367 | shapes = ["all"] 368 | 369 | [[entry]] 370 | themes = ["fire"] 371 | span = "1200-1329" 372 | shapes = ["all"] 373 | 374 | [[entry]] 375 | themes = ["forest"] 376 | span = "1330-1459" 377 | shapes = ["all"] 378 | 379 | [[entry]] 380 | themes = ["ocean"] 381 | span = "1500-1629" 382 | shapes = ["all"] 383 | 384 | [[entry]] 385 | themes = ["blood"] 386 | span = "1630-1759" 387 | shapes = ["all"] 388 | 389 | [[entry]] 390 | themes = ["dusk"] 391 | span = "1800-1929" 392 | shapes = ["all"] 393 | 394 | [[entry]] 395 | themes = ["shadow"] 396 | span = "1930-2059" 397 | shapes = ["all"] 398 | 399 | [[entry]] 400 | themes = ["night"] 401 | span = "2100-2359" 402 | shapes = ["all"] 403 | -------------------------------------------------------------------------------- /src/tesselate.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::shape::*; 3 | use crate::svg::*; 4 | use delaunator as del; 5 | use rand::rngs::ThreadRng; 6 | use std::collections::HashSet; 7 | 8 | macro_rules! set { 9 | { $( $elem:expr ),* } => { 10 | { 11 | let mut set = HashSet::new(); 12 | $( set.insert($elem); )* 13 | set 14 | } 15 | } 16 | } 17 | 18 | /// Tile the plane with a pattern that can be mapped to a 2D grid. 19 | /// This criterion applies to all tilings used here except Delaunay triangulation. 20 | fn periodic_grid_tiling(f: &Frame, gen: F, idir: Pos, jdir: Pos) -> Vec<(Pos, Path)> 21 | where 22 | F: Fn(Pos) -> Vec<(Pos, Path)>, 23 | { 24 | let mut items = Vec::new(); 25 | let center = f.center(); 26 | let mut set = set![center]; 27 | let mut stk = vec![center]; 28 | while let Some(pos) = stk.pop() { 29 | if f.is_inside(pos) { 30 | for item in gen(pos) { 31 | items.push(item); 32 | } 33 | for &(i, j) in &[(0, 1), (0, -1), (1, 0), (-1, 0)] { 34 | let p = pos + idir * i + jdir * j; 35 | if !set.contains(&p) { 36 | set.insert(p); 37 | stk.push(p); 38 | } 39 | } 40 | } 41 | } 42 | items 43 | } 44 | 45 | pub fn tile_hexagons(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 46 | let idir = Pos::polar(rot - 30, (size * 2.) * radians(30).cos()); 47 | let jdir = Pos::polar(rot + 30, (size * 2.) * radians(30).cos()); 48 | let m = Movable::hexagon(size, rot); 49 | periodic_grid_tiling(f, |p| vec![m.render(p)], idir, jdir) 50 | } 51 | 52 | pub fn tile_triangles(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 53 | let idir = Pos::polar(rot - 30, (size * 2.) * radians(30).cos()); 54 | let jdir = Pos::polar(rot + 30, (size * 2.) * radians(30).cos()); 55 | let adjust = Pos::polar(rot + 60, size * radians(30).sin()) + idir * 0.5; 56 | let m1 = Movable::triangle(size, rot + 60); 57 | let m2 = Movable::triangle(size, rot); 58 | periodic_grid_tiling(f, |p| vec![m1.render(p), m2.render(p + adjust)], idir, jdir) 59 | } 60 | 61 | pub fn tile_hybrid_hexagons_triangles(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 62 | let idir = Pos::polar(rot, size * 2.); 63 | let jdir = Pos::polar(rot + 60, size * 2.); 64 | let adjust = Pos::polar(rot + 30, size / radians(30).cos()); 65 | let m = [ 66 | Movable::hexagon(size, rot), 67 | Movable::triangle(size * radians(30).sin(), rot + 30), 68 | Movable::triangle(size * radians(30).sin(), rot + 90), 69 | ]; 70 | periodic_grid_tiling( 71 | f, 72 | |p| { 73 | vec![ 74 | m[0].render(p), 75 | m[1].render(p + adjust), 76 | m[2].render(p - adjust), 77 | ] 78 | }, 79 | idir, 80 | jdir, 81 | ) 82 | } 83 | 84 | pub fn tile_hybrid_squares_triangles(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 85 | let a = size / 2_f64.sqrt(); 86 | let b = a * radians(30).tan(); 87 | let c = a / radians(30).cos(); 88 | // 89 | // +---------------+, 90 | // | ,' |,'-, 91 | // | x' | 'c '-, 92 | // | ,' | ', '-, 93 | // | +---a---|--b--+ :- 94 | // | | ,-' 95 | // | | ,-' 96 | // | | ,-' 97 | // +---------------+' 98 | // 99 | let idir = Pos::polar(rot, c + a * 2. + 2. * b) + Pos::polar(rot + 60, c + a * 2. + 2. * b); 100 | let jdir = Pos::polar(rot, c + a * 2. + 2. * b) + Pos::polar(rot - 60, c + a * 2. + 2. * b); 101 | let mv = [ 102 | Movable::square(size, rot), 103 | Movable::square(size, rot + 60), 104 | Movable::square(size, rot - 60), 105 | Movable::triangle(c, rot + 60), 106 | Movable::triangle(c, rot), 107 | Movable::triangle(c, rot + 90), 108 | Movable::triangle(c, rot + 30), 109 | ]; 110 | periodic_grid_tiling( 111 | f, 112 | |pos| { 113 | let mut items = vec![ 114 | mv[4].render(pos + Pos::polar(rot, c + 2. * b + 2. * a)), 115 | mv[3].render(pos - Pos::polar(rot, c + 2. * b + 2. * a)), 116 | ]; 117 | for i in 0..6 { 118 | items.push(mv[3 + (i as usize % 2)].render(pos + Pos::polar(rot + i * 60, c))); 119 | items.push(mv[i as usize % 3].render(pos + Pos::polar(rot + i * 60, c + b + a))); 120 | items.push( 121 | mv[5 + (i as usize % 2)] 122 | .render(pos + Pos::polar(rot + i * 60 + 30, 2. * a + c)), 123 | ); 124 | } 125 | items 126 | }, 127 | idir, 128 | jdir, 129 | ) 130 | } 131 | 132 | pub fn tile_rhombus(f: &Frame, ldiag: f64, sdiag: f64, rot: isize) -> Vec<(Pos, Path)> { 133 | let idir = Pos::polar(rot, ldiag) + Pos::polar(rot + 90, sdiag); 134 | let jdir = Pos::polar(rot, -ldiag) + Pos::polar(rot + 90, sdiag); 135 | let m = Movable::rhombus(ldiag, sdiag, rot); 136 | periodic_grid_tiling(f, |p| vec![m.render(p)], idir, jdir) 137 | } 138 | 139 | /// External crate does the heavy lifting and is an order of magnitude faster than the previously implemented Boyer-Watson algorithm. 140 | /// Only downside is that it requires conversions between position types. 141 | fn fast_triangulate(pts: &[Pos]) -> Vec<(Pos, Pos, Pos)> { 142 | let points = pts 143 | .iter() 144 | .map(|&Pos(x, y)| del::Point { x, y }) 145 | .collect::>(); 146 | let result = del::triangulate(&points) 147 | .unwrap() 148 | .triangles 149 | .iter() 150 | .map(|&i| pts[i]) 151 | .collect::>(); 152 | let mut v = Vec::new(); 153 | for i in 0..result.len() / 3 { 154 | v.push((result[i * 3], result[i * 3 + 1], result[i * 3 + 2])); 155 | } 156 | v 157 | } 158 | 159 | pub fn random_delaunay(f: &Frame, rng: &mut ThreadRng, n: usize) -> Vec<(Pos, Path)> { 160 | let mut pts = Vec::new(); 161 | for _ in 0..n { 162 | pts.push(Pos::random(f, rng)); 163 | } 164 | let triangulation = fast_triangulate(&pts); 165 | triangulation 166 | .into_iter() 167 | .map(|(a, b, c)| { 168 | ( 169 | (a + b + c) * 0.33, 170 | Path::new(Data::new(a).with_line_to(b).with_line_to(c)), 171 | ) 172 | }) 173 | .collect::>() 174 | } 175 | 176 | pub fn pentagons_type1(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 177 | let beta = 80; 178 | let gamma = 180 - beta; 179 | let alpha = 110; 180 | let delta = 110; 181 | let epsilon = 360 - alpha - delta; 182 | let sizes = [size * 1.2, size, size * 0.9]; 183 | let angles = [alpha, beta, gamma, delta, epsilon]; 184 | #[rustfmt::skip] 185 | let mv = [ 186 | Pentagon { sizes, rot, angles }.to_movable(), 187 | Pentagon { sizes, rot: rot + 180, angles }.to_movable(), 188 | ]; 189 | let idir = mv[0].vertex(3) - mv[0].vertex(0); 190 | let jdir = mv[0].vertex(0) - mv[1].vertex(4) + mv[1].vertex(2) - mv[0].vertex(1); 191 | periodic_grid_tiling( 192 | f, 193 | |pos| { 194 | vec![ 195 | mv[0].render(pos - mv[0].vertex(0) + mv[0].side(4) * 0.5), 196 | mv[1].render(pos - mv[1].vertex(0) + mv[1].side(4) * 0.5), 197 | ] 198 | }, 199 | idir, 200 | jdir, 201 | ) 202 | } 203 | 204 | pub fn pentagons_type2(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 205 | let alpha = 110; 206 | let beta = 110; 207 | let gamma = 100; 208 | let delta = 180 - beta; 209 | let epsilon = 150; 210 | let sizes = [size, size * 1.2, size]; 211 | let angles = [epsilon, delta, gamma, beta, alpha]; 212 | let rangles = [beta, gamma, delta, epsilon, alpha]; 213 | #[rustfmt::skip] 214 | let mv = [ 215 | Pentagon { sizes, rot, angles }.to_movable(), 216 | Pentagon { sizes, rot: rot + 180, angles }.to_movable(), 217 | Pentagon { sizes, rot: rot + 180, angles: rangles }.to_movable(), 218 | Pentagon { sizes, rot, angles: rangles }.to_movable(), 219 | ]; 220 | let idir = mv[0].vertex(0) - mv[2].vertex(1) + mv[2].vertex(2) - mv[0].vertex(3); 221 | let jdir = mv[0].vertex(4) - mv[3].vertex(1) + mv[3].vertex(4) - mv[0].vertex(2); 222 | periodic_grid_tiling( 223 | f, 224 | |pos| { 225 | vec![ 226 | mv[0].render(pos), 227 | mv[1].render(pos + mv[0].vertex(2) + mv[3].side(4) - mv[1].vertex(1)), 228 | mv[2].render(pos + mv[0].vertex(0) - mv[2].vertex(1)), 229 | mv[3].render(pos + mv[0].vertex(2) - mv[3].vertex(4)), 230 | ] 231 | }, 232 | idir, 233 | jdir, 234 | ) 235 | } 236 | 237 | pub fn pentagons_type3(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 238 | let alpha = 120; 239 | let gamma = alpha; 240 | let delta = alpha; 241 | let beta = 77; 242 | let epsilon = 180 - beta; 243 | let p = 0.3; 244 | let sizes = [size * p, size, size * (1. - p)]; 245 | let angles = [epsilon, delta, gamma, beta, alpha]; 246 | #[rustfmt::skip] 247 | let mv = [ 248 | Pentagon { sizes, rot, angles }.to_movable(), 249 | Pentagon { sizes, rot: rot + 120, angles }.to_movable(), 250 | Pentagon { sizes, rot: rot - 120, angles }.to_movable(), 251 | ]; 252 | let idir = -mv[0].vertex(4) + mv[0].vertex(2) - mv[2].vertex(2) + mv[2].vertex(4); 253 | let jdir = -mv[0].vertex(4) + mv[0].vertex(2) - mv[1].vertex(2) + mv[1].vertex(4); 254 | periodic_grid_tiling( 255 | f, 256 | |pos| { 257 | vec![ 258 | mv[0].render(pos - mv[0].vertex(4)), 259 | mv[1].render(pos - mv[1].vertex(4)), 260 | mv[2].render(pos - mv[2].vertex(4)), 261 | ] 262 | }, 263 | idir, 264 | jdir, 265 | ) 266 | } 267 | 268 | pub fn pentagons_type4(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 269 | let alpha = 100; 270 | let beta = 90; 271 | let gamma = 130; 272 | let delta = 90; 273 | let epsilon = 130; 274 | let sizes = [size, size, size * 0.825]; 275 | let angles = [alpha, beta, gamma, delta, epsilon]; 276 | #[rustfmt::skip] 277 | let mv = [ 278 | Pentagon { sizes, rot, angles }.to_movable(), 279 | Pentagon { sizes, rot: rot + 90, angles }.to_movable(), 280 | Pentagon { sizes, rot: rot + 180, angles }.to_movable(), 281 | Pentagon { sizes, rot: rot - 90, angles }.to_movable(), 282 | ]; 283 | let idir = -mv[0].vertex(1) + mv[0].vertex(4) - mv[2].vertex(0) + mv[2].vertex(1); 284 | let jdir = -mv[1].vertex(1) + mv[1].vertex(4) - mv[3].vertex(0) + mv[3].vertex(1); 285 | periodic_grid_tiling( 286 | f, 287 | |pos| { 288 | vec![ 289 | mv[0].render(pos - mv[0].vertex(1)), 290 | mv[1].render(pos - mv[1].vertex(1)), 291 | mv[2].render(pos - mv[2].vertex(1)), 292 | mv[3].render(pos - mv[3].vertex(1)), 293 | ] 294 | }, 295 | idir, 296 | jdir, 297 | ) 298 | } 299 | 300 | pub fn pentagons_type5(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 301 | let alpha = 150; 302 | let beta = 60; 303 | let gamma = 120; 304 | let delta = 90; 305 | let epsilon = 120; 306 | let sizes = [size, size, size]; 307 | let angles = [delta, gamma, beta, alpha, epsilon]; 308 | #[rustfmt::skip] 309 | let mv = [ 310 | Pentagon { sizes, rot, angles }.to_movable(), 311 | Pentagon { sizes, rot: rot + 60, angles }.to_movable(), 312 | Pentagon { sizes, rot: rot + 120, angles }.to_movable(), 313 | Pentagon { sizes, rot: rot + 180, angles }.to_movable(), 314 | Pentagon { sizes, rot: rot - 120, angles }.to_movable(), 315 | Pentagon { sizes, rot: rot - 60, angles }.to_movable(), 316 | ]; 317 | let idir = -mv[1].vertex(2) + mv[1].vertex(4) - mv[5].vertex(4) + mv[5].vertex(1) 318 | - mv[1].vertex(1) 319 | + mv[1].vertex(4) 320 | - mv[3].vertex(4) 321 | + mv[3].vertex(2); 322 | let jdir = -mv[1].vertex(2) + mv[1].vertex(4) - mv[5].vertex(4) + mv[5].vertex(1) 323 | - mv[3].vertex(1) 324 | + mv[3].vertex(4) 325 | - mv[5].vertex(4) 326 | + mv[5].vertex(2); 327 | periodic_grid_tiling( 328 | f, 329 | |pos| { 330 | vec![ 331 | mv[0].render(pos - mv[0].vertex(2)), 332 | mv[1].render(pos - mv[1].vertex(2)), 333 | mv[2].render(pos - mv[2].vertex(2)), 334 | mv[3].render(pos - mv[3].vertex(2)), 335 | mv[4].render(pos - mv[4].vertex(2)), 336 | mv[5].render(pos - mv[5].vertex(2)), 337 | mv[2].render(pos - mv[0].vertex(2) + mv[0].vertex(4) - mv[2].vertex(4)), 338 | mv[3].render(pos - mv[1].vertex(2) + mv[1].vertex(4) - mv[3].vertex(4)), 339 | mv[4].render(pos - mv[2].vertex(2) + mv[2].vertex(4) - mv[4].vertex(4)), 340 | mv[5].render(pos - mv[3].vertex(2) + mv[3].vertex(4) - mv[5].vertex(4)), 341 | mv[0].render(pos - mv[4].vertex(2) + mv[4].vertex(4) - mv[0].vertex(4)), 342 | mv[1].render(pos - mv[5].vertex(2) + mv[5].vertex(4) - mv[1].vertex(4)), 343 | mv[4].render(pos - mv[0].vertex(2) + mv[0].vertex(4) - mv[4].vertex(4)), 344 | mv[5].render(pos - mv[1].vertex(2) + mv[1].vertex(4) - mv[5].vertex(4)), 345 | mv[0].render(pos - mv[2].vertex(2) + mv[2].vertex(4) - mv[0].vertex(4)), 346 | mv[1].render(pos - mv[3].vertex(2) + mv[3].vertex(4) - mv[1].vertex(4)), 347 | mv[2].render(pos - mv[4].vertex(2) + mv[4].vertex(4) - mv[2].vertex(4)), 348 | mv[3].render(pos - mv[5].vertex(2) + mv[5].vertex(4) - mv[3].vertex(4)), 349 | ] 350 | }, 351 | idir, 352 | jdir, 353 | ) 354 | } 355 | 356 | pub fn pentagons_type6(f: &Frame, size: f64, rot: isize) -> Vec<(Pos, Path)> { 357 | let rot = 60; 358 | let alpha = 150; 359 | let beta = 44; 360 | let gamma = 122; 361 | let delta = 180 - beta; 362 | let epsilon = 2 * beta; 363 | let sizes = [size, size, size]; 364 | let angles = [gamma, delta, epsilon, alpha, beta]; 365 | #[rustfmt::skip] 366 | let mv = [ 367 | Pentagon { sizes, rot, angles }.to_movable(), 368 | Pentagon { sizes, rot: rot + 180, angles }.to_movable(), 369 | Pentagon { sizes, rot: rot - beta as isize, angles }.to_movable(), 370 | Pentagon { sizes, rot: rot - beta as isize + 180, angles }.to_movable(), 371 | ]; 372 | let idir = -mv[0].vertex(3) + mv[0].vertex(2) - mv[1].vertex(1) + mv[1].vertex(4); 373 | let jdir = -mv[3].vertex(0) + mv[3].vertex(3) - mv[1].vertex(0) + mv[1].vertex(3); 374 | periodic_grid_tiling( 375 | f, 376 | |pos| { 377 | vec![ 378 | mv[0].render(pos - mv[0].vertex(3)), 379 | mv[1].render(pos - mv[1].vertex(4)), 380 | mv[2].render(pos - mv[2].vertex(0)), 381 | mv[3].render(pos - mv[3].vertex(4)), 382 | ] 383 | }, 384 | idir, 385 | jdir, 386 | ) 387 | } 388 | 389 | struct Pentagon { 390 | rot: isize, 391 | sizes: [f64; 3], 392 | angles: [usize; 5], 393 | } 394 | 395 | impl Pentagon { 396 | fn to_movable(&self) -> Movable { 397 | assert_eq!(0, { 398 | let mut s = 0; 399 | for i in 0..5 { 400 | s += 180 - self.angles[i]; 401 | } 402 | s.rem_euclid(360) 403 | }); 404 | let mut pts = Vec::new(); 405 | pts.push(Pos::zero()); 406 | let mut running_angle = self.rot; 407 | for i in 0..=2 { 408 | let latest = pts[i]; 409 | pts.push(latest + Pos::polar(running_angle, self.sizes[i])); 410 | running_angle += 180 - self.angles[i + 1] as isize; 411 | } 412 | let latest = pts[3]; 413 | pts.push(Pos::intersect( 414 | (Pos::zero(), self.rot + self.angles[0] as isize), 415 | (latest, running_angle), 416 | )); 417 | let mid = pts.iter().fold(Pos::zero(), |acc, item| acc + *item) * 0.2; 418 | Movable::from(pts.into_iter().map(|p| p - mid).collect::>()) 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /prototype/prototype.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import math 4 | from datetime import datetime 5 | from random import randint, random 6 | from math import acos, asin, pi 7 | from cmath import phase 8 | import sys 9 | 10 | try: 11 | import psutil 12 | cpu = psutil.cpu_percent() 13 | print("Current CPU usage: {}".format(cpu)) 14 | if cpu > 20: 15 | print("Delaying wallpaper to later") 16 | sys.exit(2) 17 | except ModuleNotFoundError: 18 | print("psutil not found, continuing process") 19 | 20 | def cos(n): 21 | return math.cos(math.radians(n)) 22 | 23 | def sin(n): 24 | return math.sin(math.radians(n)) 25 | 26 | cos60 = cos(60) 27 | sin60 = sin(60) 28 | 29 | def sincos(theta): 30 | return (cos(theta), sin(theta)) 31 | 32 | def timetag(m, s): 33 | return int(m/60*10000 + s/60*100) 34 | 35 | def meanpoint(add, base, weight): 36 | return tuple(a*weight + b*(1-weight) for (a, b) in zip(add, base)) 37 | 38 | def diff(a, b): 39 | return (a[0] - b[0], a[1] - b[1]) 40 | 41 | def add(a, b): 42 | return (a[0] - b[0], a[1] - b[1]) 43 | 44 | def dot(a, b): 45 | return (a[0] * b[0]) + (a[1] * b[1]) 46 | 47 | def mul(l, a): 48 | return (l * a[0], l * a[1]) 49 | 50 | def sign(p1, p2, p3): 51 | return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]) 52 | 53 | def random_color(): 54 | return (randint(0, 100), randint(0, 100), randint(0, 100)) 55 | 56 | def inside_triangle(pt, v1, v2, v3): 57 | d1 = sign(pt, v1, v2) 58 | d2 = sign(pt, v2, v3) 59 | d3 = sign(pt, v3, v1) 60 | has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) 61 | has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) 62 | return not (has_neg and has_pos); 63 | 64 | def fill_triangle(a, b, c, scene): 65 | I = len(scene) 66 | J = len(scene[0]) 67 | color = random_color() 68 | for i in range(I): 69 | for j in range(J): 70 | _, pti, ptj = scene[i][j] 71 | if inside_triangle((pti, ptj), a, b, c): 72 | scene[i][j][0] = color 73 | 74 | def random_triangle(f): 75 | center = (random(), random()) 76 | rad = random() * f + 0.1 77 | theta1 = randint(0, 360) 78 | theta2 = randint(80, 150) 79 | theta3 = randint(80, 150) 80 | def polar(c, t, r): 81 | return add(c, mul(r, sincos(t))) 82 | return (polar(center, theta1, rad), polar(center, theta1+theta2, rad), polar(center, theta1+theta2+theta3, rad)) 83 | 84 | def inside_circle(pt, c, r, ratio): 85 | cp = diff(pt, c) 86 | d = (cp[0]**2 + (cp[1]/ratio)**2)**.5 87 | return (d < r) 88 | 89 | def random_circle(f): 90 | center = (random(), random()) 91 | rad = random() * f + 0.1 92 | return (center, rad) 93 | 94 | def fill_circle(c, r, ratio, scene): 95 | I = len(scene) 96 | J = len(scene[0]) 97 | color = random_color() 98 | for i in range(I): 99 | for j in range(J): 100 | _, pti, ptj = scene[i][j] 101 | if inside_circle((pti, ptj), c, r, ratio): 102 | scene[i][j][0] = color 103 | 104 | def random_stripe(midpt, horiz=False): 105 | if horiz: 106 | return ((midpt, 0.5), randint(-10, 10) + 90) 107 | else: 108 | return ((0.5, midpt), randint(-8, 8)) 109 | 110 | def right_of_stripe(midpt, tilt, pt): 111 | line = add(midpt, sincos(tilt)) 112 | return sign(midpt, line, pt) < 0 113 | 114 | def fill_stripe(midpt, tilt, scene): 115 | I = len(scene) 116 | J = len(scene[0]) 117 | color = random_color() 118 | for i in range(I): 119 | for j in range(J): 120 | _, pti, ptj = scene[i][j] 121 | if right_of_stripe(midpt, tilt, (pti, ptj)): 122 | scene[i][j][0] = color 123 | 124 | def random_spiral(): 125 | r = random() * 0.1 + 0.05 126 | c = (randint(3, 7)/10, randint(3, 7)/10) 127 | return (c, r) 128 | 129 | def inside_spiral(c, r, ratio, pt): 130 | ci, cj = c 131 | pti, ptj = pt 132 | di, dj = ci - pti, (cj - ptj)/ratio 133 | theta = phase(dj + di*1j) 134 | radius = (di**2 + dj**2)**.5 + theta / pi * r 135 | return int(radius/r) % 2 == 0 136 | 137 | def fill_spiral(c, r, ratio, scene): 138 | I = len(scene) 139 | J = len(scene[0]) 140 | color = random_color() 141 | for i in range(I): 142 | for j in range(J): 143 | _, pti, ptj = scene[i][j] 144 | if inside_spiral(c, r, ratio, (pti, ptj)): 145 | scene[i][j][0] = color 146 | 147 | def random_lines(): 148 | r = 0.1 149 | c = (0.5, 0.5) 150 | return (c, r) 151 | 152 | def inside_lines(c, r, ratio, pt, fn, x): 153 | ci, cj = c 154 | pti, ptj = pt 155 | di, dj = ci - pti, (cj - ptj)/ratio 156 | theta = fn((di if x == 0 else dj)/((di**2 + dj**2)**.5)) 157 | radius = (di**2 + dj**2)**.5 + theta 158 | return int(radius/r) % 2 == 0 159 | 160 | def fill_lines(c, r, ratio, scene): 161 | I = len(scene) 162 | J = len(scene[0]) 163 | if random() < 0.5: 164 | fn = [acos, asin][randint(0, 1)] 165 | x = randint(0, 1) 166 | color = random_color() 167 | for i in range(I): 168 | for j in range(J): 169 | _, pti, ptj = scene[i][j] 170 | if inside_lines(c, r, ratio, (pti, ptj), fn, x): 171 | scene[i][j][0] = color 172 | else: 173 | x = randint(0, 1) 174 | color = random_color() 175 | for i in range(I): 176 | for j in range(J): 177 | _, pti, ptj = scene[i][j] 178 | if inside_lines(c, r, ratio, (pti, ptj), [acos, asin][randint(0, 1)], x): 179 | scene[i][j][0] = color 180 | 181 | 182 | def color_adjust(rgb): 183 | return (max(0, min(100, c)) for c in rgb) 184 | 185 | def color_variate(rgb, amount): 186 | return (c+randint(0, amount) for c in rgb) 187 | 188 | # def draw_hex(size, centre, color, drawing): 189 | # adjactent = size * cos60 190 | # opposite = size * sin60 191 | # half = size / 2.0 192 | # top_left = (centre[0] - half, centre[1] - opposite) 193 | # top_right = (centre[0] + half, centre[1] - opposite) 194 | # left = (centre[0] - (half + adjactent), centre[1]) 195 | # right = (centre[0] + (half + adjactent), centre[1]) 196 | # bottom_left = (centre[0] - half, centre[1] + opposite) 197 | # bottom_right = (centre[0] + half, centre[1] + opposite) 198 | # 199 | # points=[top_left, top_right, right, bottom_right, bottom_left, left] 200 | # hex = svg.shapes.Polygon(points, 201 | # stroke=svg.rgb(0, 0, 0, '%'), 202 | # stroke_width=1, 203 | # stroke_opacity=100, 204 | # fill=color, 205 | # fill_opacity=100) 206 | # drawing.add(hex) 207 | 208 | def fmt_hex(size, center, color): 209 | adjactent = size * cos60 210 | opposite = size * sin60 211 | half = size / 2.0 212 | top_left = (center[0] - half, center[1] - opposite) 213 | top_right = (center[0] + half, center[1] - opposite) 214 | left = (center[0] - (half + adjactent), center[1]) 215 | right = (center[0] + (half + adjactent), center[1]) 216 | bottom_left = (center[0] - half, center[1] + opposite) 217 | bottom_right = (center[0] + half, center[1] + opposite) 218 | 219 | return "".format(*color, *top_left, *top_right, *right, *bottom_right, *bottom_left, *left) 220 | 221 | HGT = 9 222 | WTH = 5 223 | DIG = { 224 | '0': (" ### \n# #\n# #\n# #\n# # #\n# #\n# #\n## ##\n # ", 5), 225 | '1': (" ## \n# # \n # \n # \n # \n # \n # \n#####\n # ", 5), 226 | '2': (" ### \n# #\n# #\n ##\n ## \n# \n# \n## ##\n # ", 5), 227 | '3': (" ### \n# #\n #\n ##\n ## \n #\n #\n## ##\n # ", 5), 228 | '4': (" \n# #\n# #\n## #\n ###\n #\n #\n #\n ", 5), 229 | '5': (" ### \n# #\n# \n## \n ## \n #\n# #\n## ##\n # ", 5), 230 | '6': (" ### \n# #\n# #\n# \n#### \n# #\n# #\n## ##\n # ", 5), 231 | '7': (" ### \n# #\n #\n ##\n #\n #\n #\n #\n ", 5), 232 | '8': (" ### \n# #\n# #\n## ##\n ### \n# #\n# #\n## ##\n # ", 5), 233 | '9': (" ### \n# #\n# #\n## ##\n # #\n #\n# #\n## ##\n # ", 5), 234 | 'a': (" \n \n \n ### \n# #\n ####\n# #\n## ##\n # ", 5), 235 | 'A': (" ### \n# #\n# #\n# #\n#####\n# #\n# #\n# #\n# #", 5), 236 | 'b': ("# \n# \n# \n# \n#### \n# #\n# #\n## ##\n # ", 5), 237 | 'c': (" \n \n \n \n ### \n# #\n# \n## ##\n # ", 5), 238 | 'd': (" #\n #\n #\n #\n ####\n# #\n# #\n## ##\n # ", 5), 239 | 'D': ("## \n# ## \n# #\n# #\n# #\n# #\n# ##\n### \n# ", 5), 240 | 'e': (" \n \n \n ### \n# #\n## ##\n# # \n## ##\n # ", 5), 241 | 'F': (" ### \n# #\n# \n# \n### \n# \n# \n# \n# ", 5), 242 | 'g': (" \n \n \n \n ### \n# #\n# #\n## ##\n # #\n #\n## ##\n #", 5), 243 | 'h': ("# \n# \n# \n# \n#### \n# #\n# #\n# #\n# #", 5), 244 | 'i': (" \n \n # \n # \n # \n # \n # \n#####\n # ", 5), 245 | 'J': (" ### \n# #\n #\n #\n # #\n# #\n# #\n## ##\n # ", 5), 246 | 'l': ("## \n # \n # \n # \n # \n # \n # \n # ##\n # ", 5), 247 | 'M': (" \n## ##\n# # #\n# # #\n# #\n# #\n# #\n# #\n# #", 5), 248 | 'n': (" \n \n \n \n#### \n# #\n# #\n# #\n# #", 5), 249 | 'N': ("# #\n# #\n## #\n# ###\n# #\n# #\n# #\n# #\n# #", 5), 250 | 'o': (" \n \n \n \n ### \n# #\n# #\n## ##\n # ", 5), 251 | 'O': (" ### \n# #\n# #\n# #\n# #\n# #\n# #\n## ##\n # ", 5), 252 | 'p': (" \n \n \n \n ### \n# #\n# #\n## ##\n# # \n#\n#\n#", 5), 253 | 'r': (" \n \n \n \n#### \n# #\n# \n# \n# ", 5), 254 | 'S': (" ### \n# #\n# #\n## \n ## \n #\n# #\n## ##\n # ", 5), 255 | 't': ("# \n# # \n### \n# \n# \n# \n# \n## ##\n # ", 5), 256 | 'T': (" ### \n# # #\n # \n # \n # \n # \n # \n # \n # ", 5), 257 | 'u': (" \n \n \n \n# #\n# #\n# #\n## ##\n # ", 5), 258 | 'v': (" \n \n \n \n# #\n## ##\n # # \n # \n # ", 5), 259 | 'W': ("# #\n# #\n# #\n# #\n# #\n# #\n# # #\n#####\n# #", 5), 260 | 'y': (" \n \n \n \n# #\n# #\n# #\n## ##\n # #\n #\n## ##\n #", 5), 261 | ':': (" \n \n \n#\n \n#\n \n \n ", 1), 262 | '.': (" \n \n \n \n \n \n \n \n#", 1), 263 | ' ': (" \n \n \n \n \n \n \n \n ", 1), 264 | } 265 | 266 | def draw_digit(scene, chr, i0, j0): 267 | i, j = i0, j0 268 | for c in chr: 269 | if c == '#': 270 | s = 100 271 | scene[i][j][0] = (s, s, s) 272 | j += 1 273 | elif c == '\n': 274 | i += 1 275 | j = j0 276 | else: 277 | j += 1 278 | 279 | def add_timestamp(scene, time): 280 | I = len(scene) 281 | J = len(scene[0]) 282 | Iref = I // 2 - HGT // 2 - 8 283 | Jref = J // 2 - 10 284 | for c in time: 285 | fmt, wth = DIG[c] 286 | draw_digit(scene, fmt, Iref, Jref) 287 | Jref += wth + 1 288 | 289 | def make_date(date): 290 | w, m, d = list(map(int, date.strftime("%w %m %d").split(" "))) 291 | return "{}. {}. {}".format( 292 | ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][w], 293 | ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][m], 294 | d 295 | ) 296 | 297 | 298 | def add_date(scene, date): 299 | I = len(scene) 300 | J = len(scene[0]) 301 | Iref = I // 2 + HGT // 2 + 4 302 | Jref = J // 2 - 24 303 | for c in date: 304 | fmt, wth = DIG[c] 305 | draw_digit(scene, fmt, Iref, Jref) 306 | Jref += wth + 1 307 | 308 | 309 | size = 14 310 | I = 43 311 | J = 94 312 | 313 | def choose_metatheme(now): 314 | if now < timetag(5, 0): return (0, 0, 30) 315 | elif now < timetag(5, 30): return (30, 0, 20) 316 | elif now < timetag(6, 0): return (60, 0, 20) 317 | elif now < timetag(6, 30): return (90, 50, 0) 318 | elif now < timetag(7, 0): return (80, 95, 0) 319 | elif now < timetag(8, 0): return (0, 75, 0) 320 | elif now < timetag(9, 0): return (60, 75, 0) 321 | elif now < timetag(10, 0): return (45, 100, 75) 322 | elif now < timetag(11, 0): return (0, 80, 100) 323 | elif now < timetag(12, 0): return (0, 100, 90) 324 | elif now < timetag(13, 0): return (0, 40, 100) 325 | elif now < timetag(14, 0): return (0, 100, 100) 326 | elif now < timetag(15, 0): return (20, 60, 100) 327 | elif now < timetag(16, 0): return (0, 0, 90) 328 | elif now < timetag(17, 0): return (30, 30, 80) 329 | elif now < timetag(18, 0): return (15, 15, 0) 330 | elif now < timetag(19, 0): return (90, 30, 0) 331 | elif now < timetag(20, 0): return (40, 0, 20) 332 | elif now < timetag(21, 0): return (0, 0, 50) 333 | elif now < timetag(22, 0): return (20, 0, 20) 334 | elif now < timetag(23, 0): return (0, 0, 20) 335 | else: return (0, 0, 0) 336 | 337 | def calc_i(i, j): 338 | return (2*i+(1 if j%2==0 else 0))*size*sin60 339 | 340 | def calc_j(i, j): 341 | return 3*j*size*cos60 342 | 343 | def draw_img(name, scene, theme): 344 | with open(name, 'w') as f: 345 | f.write(""" 346 | """) 347 | #dwg = svg.Drawing(filename=name, debug=False, size=(2100, 1000)) 348 | for i in range(I): 349 | for j in range(J): 350 | offset = 0 if j%2==0 else 1 351 | color = scene[i][j][0] 352 | pti, ptj = calc_i(i, j), calc_j(i, j) 353 | #draw_hex(size, (ptj, pti), svg.rgb(*color, '%'), dwg) 354 | f.write(fmt_hex(size, (ptj, pti), color)) 355 | f.write("""""") 356 | #dwg.save() 357 | 358 | def main(): 359 | now = timetag(int(datetime.now().strftime("%H")), int(datetime.now().strftime("%M"))) 360 | 361 | metatheme = choose_metatheme(now) 362 | print("metatheme:", metatheme) 363 | 364 | theme1 = meanpoint(random_color(), metatheme, 0.3) 365 | theme2 = meanpoint(random_color(), metatheme, 0.3) 366 | print("themes:", theme1, theme2) 367 | 368 | max_i = calc_i(I, J) 369 | max_j = calc_j(I, J) 370 | scene = [[[theme1, calc_i(i, j)/max_i, calc_j(i, j)/max_j] for j in range(J)] for i in range(I)] 371 | 372 | style = randint(0, 2) 373 | if style == 0: 374 | # Triangles 375 | N = 20 376 | for n in range(N): 377 | fill_triangle(*random_triangle((1-n/N)/2), scene) 378 | elif style == 1: 379 | # Circles 380 | if random() < 0.33: 381 | # Everywhere 382 | N = 10 383 | for n in range(N): 384 | fill_circle(*random_circle(((1-n/N)/4 + 0.1)), max_i/max_j, scene) 385 | elif random() < 0.5: 386 | # Centered 387 | N = 10 388 | ci = randint(3, 7)/10 389 | cj = randint(3, 7)/10 390 | for n in range(N + N//2): 391 | fill_circle((ci, cj), 1-n/N, max_i/max_j, scene) 392 | else: 393 | N = 3 394 | for n in range(N): 395 | fill_spiral(*random_spiral(), max_i/max_j, scene) 396 | elif style == 2: 397 | # Stripes 398 | if random() < 0.5: 399 | # 1 direction 400 | N = 20 401 | h = random() < 0.5 402 | for n in range(N): 403 | fill_stripe(*random_stripe(1-n/N if h else n/N, horiz=h), scene) 404 | elif random() < 0.5: 405 | # Bidirectional 406 | N = 10 407 | for n in range(N): 408 | fill_stripe(*random_stripe(1-n/N, horiz=True), scene) 409 | fill_stripe(*random_stripe(n/N, horiz=False), scene) 410 | else: 411 | # Slanted 412 | N = 20 413 | for n in range(N): 414 | fill_stripe((n/N, n/N), randint(-50, -40), scene) 415 | else: 416 | print("Unimplemented") 417 | sys.exit(3) 418 | for i in range(50): 419 | scene[randint(0, I-1)][randint(0, J-1)][0] = random_color() 420 | 421 | for i in range(I): 422 | for j in range(J): 423 | scene[i][j][0] = color_adjust(meanpoint(color_variate(scene[i][j][0], 20), theme2, 0.3)) 424 | 425 | add_timestamp(scene, datetime.now().strftime("%H:%M")) 426 | add_date(scene, make_date(datetime.today())) 427 | print("Done rendering, now saving image") 428 | draw_img('/tmp/wallpaper-random.svg', scene, theme2) 429 | 430 | main() 431 | 432 | # TODO: 433 | # - waves 434 | # - stripes 435 | -------------------------------------------------------------------------------- /src/deserializer.rs: -------------------------------------------------------------------------------- 1 | use crate::cfg::SceneCfg; 2 | use crate::prelude::*; 3 | use rand::{rngs::ThreadRng, seq::SliceRandom}; 4 | use serde_derive::Deserialize; 5 | use std::collections::HashMap; 6 | use toml::{map::Map, Value}; 7 | 8 | const BASE_WEIGHT: usize = 10; 9 | 10 | /// All config information 11 | #[derive(Deserialize, Default, Debug)] 12 | pub struct MetaConfig { 13 | pub global: Option, 14 | pub lines: Option, 15 | pub colors: Option, 16 | pub themes: Option, 17 | pub shapes: Option, 18 | pub data: Option, 19 | pub entry: Option>, 20 | } 21 | 22 | /// Global options 23 | #[derive(Deserialize, Default, Debug)] 24 | pub struct ConfigGlobal { 25 | pub deviation: Option, 26 | pub weight: Option, // Artifact of previous name 27 | pub distance: Option, 28 | pub size: Option, 29 | pub width: Option, 30 | pub height: Option, 31 | } 32 | 33 | /// Lines appearance 34 | #[derive(Deserialize, Default, Debug)] 35 | pub struct ConfigLines { 36 | pub width: Option, 37 | pub color: Option, 38 | pub del_width: Option, 39 | pub del_color: Option, 40 | pub hex_width: Option, 41 | pub hex_color: Option, 42 | pub tri_width: Option, 43 | pub tri_color: Option, 44 | pub rho_width: Option, 45 | pub rho_color: Option, 46 | pub hex_and_tri_width: Option, 47 | pub hex_and_tri_color: Option, 48 | pub squ_and_tri_width: Option, 49 | pub squ_and_tri_color: Option, 50 | pub pen_width: Option, 51 | pub pen_color: Option, 52 | } 53 | 54 | /// Color list 55 | #[derive(Deserialize, Default, Debug)] 56 | pub struct ConfigColors { 57 | #[serde(flatten)] 58 | pub list: Map, 59 | } 60 | 61 | /// Theme list 62 | #[derive(Deserialize, Default, Debug)] 63 | pub struct ConfigThemes { 64 | #[serde(flatten)] 65 | pub list: Map, 66 | } 67 | 68 | /// Shapes combination list 69 | #[derive(Deserialize, Default, Debug)] 70 | pub struct ConfigShapes { 71 | #[serde(flatten)] 72 | pub list: Map, 73 | } 74 | 75 | /// Group together pattern options and tiling options 76 | #[derive(Deserialize, Default, Debug)] 77 | pub struct ConfigData { 78 | pub patterns: Option, 79 | pub tilings: Option, 80 | } 81 | 82 | /// Tiling options 83 | #[derive(Deserialize, Default, Debug)] 84 | pub struct ConfigTilings { 85 | pub size_hex: Option, 86 | pub size_tri: Option, 87 | pub size_hex_and_tri: Option, 88 | pub size_squ_and_tri: Option, 89 | pub size_rho: Option, 90 | pub size_pen: Option, 91 | pub nb_delaunay: Option, 92 | } 93 | 94 | /// Pattern options 95 | #[derive(Deserialize, Default, Debug)] 96 | pub struct ConfigPatterns { 97 | pub nb_free_circles: Option, 98 | pub nb_free_spirals: Option, 99 | pub nb_free_stripes: Option, 100 | pub nb_crossed_stripes: Option, 101 | pub nb_parallel_stripes: Option, 102 | pub nb_concentric_circles: Option, 103 | pub nb_free_triangles: Option, 104 | pub nb_parallel_waves: Option, 105 | pub nb_parallel_sawteeth: Option, 106 | pub var_parallel_stripes: Option, 107 | pub var_crossed_stripes: Option, 108 | pub width_spiral: Option, 109 | pub width_stripe: Option, 110 | pub width_wave: Option, 111 | pub width_sawtooth: Option, 112 | pub tightness_spiral: Option, 113 | } 114 | 115 | /// Entry for a single theme/time combination 116 | #[derive(Deserialize, Debug)] 117 | pub struct ConfigEntry { 118 | pub span: Option, 119 | pub distance: Option, 120 | pub themes: Option>, 121 | pub shapes: Option>, 122 | pub line_color: Option, 123 | } 124 | 125 | impl MetaConfig { 126 | /// Parse from TOML. 127 | /// Heavy lifting done by external crates 128 | pub fn from_string(src: String, verbose: Verbosity) -> Self { 129 | toml::from_str(src.as_str()).unwrap_or_else(|e| { 130 | if verbose.warn { 131 | println!("No valid config file found, picking default settings"); 132 | println!("Message: {}", e); 133 | } 134 | MetaConfig::default() 135 | }) 136 | } 137 | 138 | /// Choose options at random according to configuration 139 | pub fn pick_cfg(self, rng: &mut ThreadRng, time: usize, verbose: Verbosity) -> SceneCfg { 140 | // Read default/overriden global options 141 | let (deviation, distance, size, width, height) = { 142 | let (deviation, distance, size, width, height); 143 | match self.global { 144 | None => { 145 | if verbose.info { 146 | println!("Default global"); 147 | } 148 | deviation = DEVIATION; 149 | distance = DISTANCE; 150 | size = SIZE; 151 | width = WIDTH; 152 | height = HEIGHT; 153 | } 154 | Some(g) => { 155 | match g.deviation { 156 | None => { 157 | if verbose.info { 158 | println!("Default global.deviation"); 159 | } 160 | deviation = DEVIATION; 161 | } 162 | Some(d) => deviation = d, 163 | } 164 | match g.distance { 165 | None => { 166 | distance = g.weight.unwrap_or_else(|| { 167 | if verbose.info { 168 | println!("Default global.distance"); 169 | } 170 | DISTANCE 171 | }); 172 | } 173 | Some(w) => distance = w, 174 | } 175 | match g.size { 176 | None => { 177 | if verbose.info { 178 | println!("Default global.size"); 179 | } 180 | size = SIZE; 181 | } 182 | Some(s) => { 183 | size = s; 184 | } 185 | } 186 | match g.width { 187 | None => { 188 | if verbose.info { 189 | println!("Default global.width"); 190 | } 191 | width = WIDTH; 192 | } 193 | Some(w) => { 194 | width = w; 195 | } 196 | } 197 | match g.height { 198 | None => { 199 | if verbose.info { 200 | println!("Default global.height"); 201 | } 202 | height = HEIGHT; 203 | } 204 | Some(s) => { 205 | height = s; 206 | } 207 | } 208 | } 209 | } 210 | if verbose.details { 211 | println!( 212 | "Global settings: 213 | Deviation (color) {} 214 | Distance (color) {} 215 | Size (tiles) {} 216 | Width (image) {} 217 | Height (image) {}", 218 | deviation, distance, size, width, height 219 | ); 220 | } 221 | (deviation, distance, size, width, height) 222 | }; 223 | 224 | // Get list of named colors 225 | let colors = { 226 | let mut colors = HashMap::new(); 227 | if let Some(ConfigColors { list }) = self.colors { 228 | for name in list.keys() { 229 | match color_from_value(&list[name], &colors) { 230 | Ok(c) => { 231 | if verbose.details { 232 | println!("Added new color to list: '{} = {}'", &name, &c); 233 | } 234 | colors.insert(name.clone(), c); 235 | } 236 | Err(s) => { 237 | if verbose.warn { 238 | println!("{}", s); 239 | } 240 | } 241 | } 242 | } 243 | } 244 | colors 245 | }; 246 | 247 | // Get list of named themes 248 | let mut themes = { 249 | let mut themes = HashMap::new(); 250 | if let Some(ConfigThemes { list }) = self.themes { 251 | for name in list.keys() { 252 | match theme_from_value(&list[name], &colors, &themes, verbose) { 253 | Ok(th) => { 254 | if verbose.details { 255 | println!("Added new theme to list: '{}'", &name); 256 | } 257 | themes.insert(name.clone(), th); 258 | } 259 | Err(s) => { 260 | if verbose.warn { 261 | println!("{}", s); 262 | } 263 | } 264 | } 265 | } 266 | } 267 | themes 268 | }; 269 | 270 | // List of allowed shape combinations 271 | let shapes = { 272 | let mut shapes = HashMap::new(); 273 | if let Some(ConfigShapes { list }) = self.shapes { 274 | for name in list.keys() { 275 | if verbose.details { 276 | println!("Added new shapes to list: '{}'", &name); 277 | } 278 | shapes.insert(name.clone(), shapes_from_value(&list[name], &shapes)); 279 | } 280 | } 281 | shapes 282 | }; 283 | 284 | let (theme, shape, line_color_override) = choose_theme_shapes(rng, &self.entry, time); 285 | if verbose.info { 286 | println!("Chosen theme: '{}'", &theme); 287 | } 288 | 289 | let (tiling, pattern) = match shapes.get(&shape) { 290 | None => (Tiling::choose(rng), Pattern::choose(rng)), 291 | Some(t) => ( 292 | t.1.choose(rng).unwrap_or_else(|| Tiling::choose(rng)), 293 | t.0.choose(rng).unwrap_or_else(|| Pattern::choose(rng)), 294 | ), 295 | }; 296 | if verbose.info { 297 | println!( 298 | "Pattern '{:?}' and tiling '{:?}' chosen from shapes '{}'", 299 | pattern, tiling, shape 300 | ); 301 | } 302 | 303 | // Get pattern-specific information according to picked shapes 304 | let (nb_pattern, var_stripes, width_pattern, tightness_spiral) = { 305 | let nb_pattern; 306 | let (mut var_stripes, mut width_pattern, mut tightness_spiral) = (0, 0.0, 0.0); 307 | if let Some(ConfigData { 308 | patterns: Some(p), 309 | tilings: _, 310 | }) = &self.data 311 | { 312 | match pattern { 313 | Pattern::FreeCircles => { 314 | nb_pattern = p.nb_free_circles.unwrap_or(NB_FREE_CIRCLES); 315 | } 316 | Pattern::FreeTriangles => { 317 | nb_pattern = p.nb_free_triangles.unwrap_or(NB_FREE_TRIANGLES); 318 | } 319 | Pattern::FreeStripes => { 320 | nb_pattern = p.nb_free_stripes.unwrap_or(NB_FREE_STRIPES); 321 | width_pattern = p.width_stripe.unwrap_or(WIDTH_STRIPE); 322 | } 323 | Pattern::FreeSpirals => { 324 | nb_pattern = p.nb_free_spirals.unwrap_or(NB_FREE_SPIRALS); 325 | width_pattern = p.width_spiral.unwrap_or(WIDTH_SPIRAL); 326 | tightness_spiral = p.tightness_spiral.unwrap_or(TIGHTNESS_SPIRAL); 327 | } 328 | Pattern::ConcentricCircles => { 329 | nb_pattern = p.nb_concentric_circles.unwrap_or(NB_CONCENTRIC_CIRCLES); 330 | } 331 | Pattern::ParallelStripes => { 332 | nb_pattern = p.nb_parallel_stripes.unwrap_or(NB_PARALLEL_STRIPES); 333 | var_stripes = p.var_parallel_stripes.unwrap_or(VAR_PARALLEL_STRIPES); 334 | } 335 | Pattern::CrossedStripes => { 336 | nb_pattern = p.nb_crossed_stripes.unwrap_or(NB_CROSSED_STRIPES); 337 | var_stripes = p.var_crossed_stripes.unwrap_or(VAR_CROSSED_STRIPES); 338 | } 339 | Pattern::ParallelWaves => { 340 | nb_pattern = p.nb_parallel_waves.unwrap_or(NB_PARALLEL_WAVES); 341 | width_pattern = p.width_wave.unwrap_or(WIDTH_WAVE); 342 | } 343 | Pattern::ParallelSawteeth => { 344 | nb_pattern = p.nb_parallel_sawteeth.unwrap_or(NB_PARALLEL_SAWTEETH); 345 | width_pattern = p.width_sawtooth.unwrap_or(WIDTH_SAWTOOTH); 346 | } 347 | } 348 | } else { 349 | match pattern { 350 | Pattern::FreeCircles => nb_pattern = NB_FREE_CIRCLES, 351 | Pattern::FreeTriangles => nb_pattern = NB_FREE_TRIANGLES, 352 | Pattern::FreeStripes => { 353 | nb_pattern = NB_FREE_STRIPES; 354 | width_pattern = WIDTH_STRIPE; 355 | } 356 | Pattern::FreeSpirals => { 357 | nb_pattern = NB_FREE_SPIRALS; 358 | width_pattern = WIDTH_SPIRAL; 359 | tightness_spiral = TIGHTNESS_SPIRAL; 360 | } 361 | Pattern::ConcentricCircles => nb_pattern = NB_CONCENTRIC_CIRCLES, 362 | Pattern::ParallelStripes => { 363 | nb_pattern = NB_PARALLEL_STRIPES; 364 | var_stripes = VAR_PARALLEL_STRIPES; 365 | } 366 | Pattern::CrossedStripes => { 367 | nb_pattern = NB_CROSSED_STRIPES; 368 | var_stripes = VAR_CROSSED_STRIPES; 369 | } 370 | Pattern::ParallelWaves => { 371 | nb_pattern = NB_PARALLEL_WAVES; 372 | width_pattern = WIDTH_WAVE; 373 | } 374 | Pattern::ParallelSawteeth => { 375 | nb_pattern = NB_PARALLEL_SAWTEETH; 376 | width_pattern = WIDTH_SAWTOOTH; 377 | } 378 | } 379 | } 380 | if verbose.details { 381 | println!( 382 | "Number of patterns: {} 383 | Variability of stripes orientation: {} 384 | Width of pattern: {}", 385 | nb_pattern, var_stripes, width_pattern 386 | ); 387 | } 388 | (nb_pattern, var_stripes, width_pattern, tightness_spiral) 389 | }; 390 | 391 | if themes.is_empty() { 392 | if verbose.warn { 393 | println!("No themes available. Populating with random theme"); 394 | } 395 | if colors.is_empty() { 396 | if verbose.warn { 397 | println!("No colors available. Populating with random color"); 398 | } 399 | themes.insert( 400 | String::from("-default-"), 401 | Chooser::new(vec![( 402 | ThemeItem(Color::random(rng), None, None, Salt::none()), 403 | BASE_WEIGHT, 404 | )]), 405 | ); 406 | } else { 407 | themes.insert( 408 | String::from("-default-"), 409 | Chooser::new(vec![( 410 | ThemeItem( 411 | *colors 412 | .get(*colors.keys().collect::>().choose(rng).unwrap()) 413 | .unwrap(), 414 | None, 415 | None, 416 | Salt::none(), 417 | ), 418 | BASE_WEIGHT, 419 | )]), 420 | ); 421 | } 422 | } 423 | 424 | // Get tiling-specific options according to picked shapes 425 | let (size_tiling, nb_delaunay) = { 426 | if let Some(ConfigData { 427 | patterns: _, 428 | tilings: Some(t), 429 | }) = self.data 430 | { 431 | match tiling { 432 | Tiling::Hexagons => (t.size_hex.unwrap_or(size), 0), 433 | Tiling::Triangles => (t.size_tri.unwrap_or(size), 0), 434 | Tiling::HexagonsAndTriangles => (t.size_hex_and_tri.unwrap_or(size), 0), 435 | Tiling::SquaresAndTriangles => (t.size_squ_and_tri.unwrap_or(size), 0), 436 | Tiling::Rhombus => (t.size_rho.unwrap_or(size), 0), 437 | Tiling::Pentagons(_) => (t.size_pen.unwrap_or(size), 0), 438 | Tiling::Delaunay => (0.0, t.nb_delaunay.unwrap_or(NB_DELAUNAY)), 439 | } 440 | } else { 441 | match tiling { 442 | Tiling::Hexagons => (size, 0), 443 | Tiling::Triangles => (size, 0), 444 | Tiling::HexagonsAndTriangles => (size, 0), 445 | Tiling::SquaresAndTriangles => (size, 0), 446 | Tiling::Rhombus => (size, 0), 447 | Tiling::Pentagons(_) => (size, 0), 448 | Tiling::Delaunay => (0.0, NB_DELAUNAY), 449 | } 450 | } 451 | }; 452 | if verbose.details { 453 | println!( 454 | "Tiling size: {} 455 | Delaunay triangles count: {}", 456 | size_tiling, nb_delaunay 457 | ); 458 | } 459 | let (line_width, line_color_default) = { 460 | if let Some(lines) = self.lines { 461 | lines.get_settings(tiling, &colors) 462 | } else { 463 | (LINE_WIDTH, LINE_COLOR) 464 | } 465 | }; 466 | if verbose.details { 467 | println!( 468 | "Line width: {} 469 | Line color: {}", 470 | line_width, line_color_default 471 | ); 472 | } 473 | 474 | SceneCfg { 475 | deviation, 476 | distance, 477 | theme: themes 478 | .get(&theme) 479 | .unwrap_or_else(|| { 480 | themes 481 | .get(*themes.keys().collect::>().choose(rng).unwrap()) 482 | .unwrap() 483 | }) 484 | .clone(), 485 | frame: Frame { 486 | x: 0, 487 | y: 0, 488 | w: width, 489 | h: height, 490 | }, 491 | tiling, 492 | line_width, 493 | line_color: color_from_value(&Value::String(line_color_override), &colors) 494 | .unwrap_or_else(|_| { 495 | color_from_value(&Value::String(line_color_default.to_string()), &colors) 496 | .unwrap_or(Color(0, 0, 0)) 497 | }), 498 | pattern, 499 | nb_pattern, 500 | var_stripes, 501 | nb_delaunay, 502 | size_tiling, 503 | width_pattern, 504 | tightness_spiral, 505 | } 506 | } 507 | } 508 | 509 | /// Parse a color code: decimal (0-255) or hex (00-FF) 510 | fn color_from_value(val: &Value, dict: &HashMap) -> Result { 511 | match val { 512 | Value::String(s) => { 513 | if let Some(color) = dict.get(s.as_str()) { 514 | return Ok(*color); 515 | } 516 | if s.len() == 7 && &s[0..1] == "#" { 517 | let r = usize::from_str_radix(&s[1..3], 16); 518 | let g = usize::from_str_radix(&s[3..5], 16); 519 | let b = usize::from_str_radix(&s[5..7], 16); 520 | match (r, g, b) { 521 | (Ok(r), Ok(g), Ok(b)) => Ok(Color(r, g, b)), 522 | _ => Err(format!( 523 | "{:?} is not a valid color format.\nUse [0, 0, 255] or \"#0000FF\"", 524 | s 525 | )), 526 | } 527 | } else { 528 | Err(format!( 529 | "{:?} is not a valid color format.\nUse [0, 0, 255] or \"#0000FF\"", 530 | s 531 | )) 532 | } 533 | } 534 | Value::Array(arr) => { 535 | if arr.len() != 3 { 536 | return Err(format!( 537 | "{:?} is not a valid color format.\nUse [0, 0, 255] or \"#0000FF\"", 538 | val 539 | )); 540 | } 541 | match &arr[0..3] { 542 | [Value::Integer(r), Value::Integer(g), Value::Integer(b)] => { 543 | Ok(Color(*r as usize, *g as usize, *b as usize)) 544 | } 545 | _ => Err(format!( 546 | "{:?} is not a valid color format.\nUse [0, 0, 255] or \"#0000FF\"", 547 | arr 548 | )), 549 | } 550 | } 551 | _ => Err(format!( 552 | "{:?} is not a valid color format.\nUse [0, 0, 255] or \"#0000FF\"", 553 | val 554 | )), 555 | } 556 | } 557 | 558 | fn theme_item_from_value( 559 | val: &Value, 560 | dict: &HashMap, 561 | verbose: Verbosity, 562 | ) -> (ThemeItem, usize) { 563 | let warn_invalid = |x| { 564 | if verbose.warn { 565 | println!( 566 | "Invalid item ({:?}) 567 | Provide one of: 568 | - a named color (\"blue\") 569 | - a hex code (\"#0000FF\") 570 | - any of the above along with an integer weight (\" xWEIGHT\") 571 | - any of the above along with a variability override (\" ~VAR\") 572 | - any of the above along with a distance override (\" !DISTANCE\") 573 | - a map item ({{ color, variability, weight, distance }}) 574 | Note that the format [, , ] is not accepted here", 575 | x 576 | ); 577 | } 578 | }; 579 | match val { 580 | Value::String(s) => { 581 | let mut color = Color(0, 0, 0); 582 | let mut wht = BASE_WEIGHT; 583 | let mut var = None; 584 | let mut dist = None; 585 | for item in s.split(' ') { 586 | if item.is_empty() { 587 | continue; 588 | } 589 | if &item[0..1] == "x" { 590 | wht = item[1..].parse().unwrap_or_else(|_| { 591 | if verbose.warn { 592 | println!("Not a valid ponderation: {}", &item[1..]); 593 | } 594 | BASE_WEIGHT 595 | }); 596 | } else if &item[0..1] == "~" { 597 | var = item[1..].parse::().map(Some).unwrap_or_else(|_| { 598 | if verbose.warn { 599 | println!("Not a valid variability: {}", &item[1..]); 600 | } 601 | None 602 | }); 603 | } else if &item[0..1] == "!" { 604 | dist = item[1..].parse::().map(Some).unwrap_or_else(|_| { 605 | if verbose.warn { 606 | println!("Not a valid distance: {}", &item[1..]); 607 | } 608 | None 609 | }); 610 | } else { 611 | match color_from_value(&Value::String(item.to_string()), dict) { 612 | Ok(c) => color = c, 613 | Err(e) => { 614 | warn_invalid(e); 615 | } 616 | } 617 | } 618 | } 619 | (ThemeItem(color, var, dist, Salt::none()), wht) 620 | } 621 | Value::Table(map) => { 622 | let color = match map.get("color") { 623 | Some(val) => match color_from_value(val, dict) { 624 | Ok(c) => c, 625 | Err(e) => { 626 | warn_invalid(e); 627 | Color(0, 0, 0) 628 | } 629 | }, 630 | None => Color(0, 0, 0), 631 | }; 632 | let var = (match map.get("variability") { 633 | Some(Value::Integer(v)) => Some(*v), 634 | Some(Value::Float(v)) => Some(v.round() as i64), 635 | Some(x) => { 636 | if verbose.warn { 637 | println!("Not a valid variability: {:?}", x); 638 | } 639 | None 640 | } 641 | None => None, 642 | }) 643 | .map(|n| n.max(0) as usize); 644 | let dist = (match map.get("distance") { 645 | Some(Value::Integer(d)) => Some(*d), 646 | Some(Value::Float(d)) => Some(d.round() as i64), 647 | Some(x) => { 648 | if verbose.warn { 649 | println!("Not a valid distance: {:?}", x); 650 | } 651 | None 652 | } 653 | None => None, 654 | }) 655 | .map(|n| n.max(0) as usize); 656 | let wht = match map.get("weight") { 657 | Some(Value::Integer(w)) => *w.max(&0) as usize, 658 | Some(Value::Float(w)) => w.round().max(0.0) as usize, 659 | Some(x) => { 660 | if verbose.warn { 661 | println!("Not a valid weight: {:?}", x); 662 | } 663 | BASE_WEIGHT 664 | } 665 | None => BASE_WEIGHT, 666 | }; 667 | let salt = match map.get("salt") { 668 | None => Salt::none(), 669 | Some(Value::Array(vec)) => { 670 | let mut salt = Salt::default(); 671 | for item in vec.iter() { 672 | if let Value::Table(tbl) = item { 673 | let color = tbl 674 | .get("color") 675 | .map(|v| { 676 | color_from_value(v, &dict).unwrap_or_else(|_| { 677 | if verbose.warn { 678 | println!("Invalid color: {:?}", v) 679 | } 680 | Color(0, 0, 0) 681 | }) 682 | }) 683 | .unwrap_or(Color(0, 0, 0)); 684 | let likeliness = match tbl.get("likeliness") { 685 | None => 1.0, 686 | Some(Value::Float(f)) => *f, 687 | Some(Value::Integer(n)) => *n as f64, 688 | Some(v) => { 689 | if verbose.warn { 690 | println!("Not a valid likeliness: {:?}", v); 691 | } 692 | 1.0 693 | } 694 | }; 695 | let variability = match tbl.get("variability") { 696 | None => 0, 697 | Some(Value::Integer(n)) => { 698 | if *n > 0 { 699 | *n as usize 700 | } else { 701 | 0 702 | } 703 | } 704 | Some(Value::Float(f)) => { 705 | if *f > 0. { 706 | f.round() as usize 707 | } else { 708 | 0 709 | } 710 | } 711 | Some(v) => { 712 | if verbose.warn { 713 | println!("Not a valid variability: {:?}", v); 714 | } 715 | 0 716 | } 717 | }; 718 | salt.0.push(SaltItem { 719 | color, 720 | likeliness, 721 | variability, 722 | }); 723 | } 724 | } 725 | salt 726 | } 727 | _ => { 728 | if verbose.warn { 729 | println!("Invalid Salt. Expected an array."); 730 | } 731 | Salt::none() 732 | } 733 | }; 734 | (ThemeItem(color, var, dist, salt), wht) 735 | } 736 | val => { 737 | warn_invalid(val.to_string()); 738 | ( 739 | ThemeItem(Color(0, 0, 0), None, None, Salt::none()), 740 | BASE_WEIGHT, 741 | ) 742 | } 743 | } 744 | } 745 | 746 | /// Read group of colors as a theme 747 | fn theme_from_value( 748 | v: &Value, 749 | colors: &ColorList, 750 | themes: &ThemeList, 751 | verbose: Verbosity, 752 | ) -> Result, String> { 753 | let mut items = Vec::new(); 754 | if let Value::String(s) = v { 755 | if let Some(th) = themes.get(s) { 756 | items = th.extract(); 757 | } 758 | } 759 | match v { 760 | Value::Array(a) => { 761 | for x in a { 762 | if let Value::String(s) = x { 763 | if let Some(th) = themes.get(s) { 764 | items.append(&mut th.extract()); 765 | continue; 766 | } 767 | } 768 | let (item, weight) = theme_item_from_value(x, colors, verbose); 769 | items.push((item, weight)); 770 | } 771 | Ok(Chooser::new(items)) 772 | } 773 | _ => Err(format!( 774 | "{:?} is not a valid theme. 775 | Provide a theme item or an array of theme items", 776 | v 777 | )), 778 | } 779 | } 780 | 781 | fn shapes_from_value( 782 | val: &Value, 783 | shapes: &HashMap, Chooser)>, 784 | ) -> (Chooser, Chooser) { 785 | let mut tilings = Chooser::new(vec![]); 786 | let mut patterns = Chooser::new(vec![]); 787 | match val { 788 | Value::Array(arr) => { 789 | for x in arr { 790 | match x { 791 | Value::String(s) => { 792 | if let Some(sh) = shapes.get(s) { 793 | let (p, t) = sh; 794 | tilings.append(t.extract()); 795 | patterns.append(p.extract()); 796 | } else { 797 | add_shape(&s[..], BASE_WEIGHT, &mut tilings, &mut patterns); 798 | } 799 | } 800 | Value::Array(a) => { 801 | if a.len() == 2 { 802 | match &a[..] { 803 | [Value::String(s), Value::Integer(w)] if *w > 0 => { 804 | add_shape(&s[..], *w as usize, &mut tilings, &mut patterns) 805 | } 806 | _ => println!("{} is not a valid shape.", x), 807 | } 808 | } else { 809 | println!("{} is not a valid shape.", x); 810 | } 811 | } 812 | _ => println!("{} is not a valid shape.", x), 813 | } 814 | } 815 | } 816 | _ => println!("{} is not an array of shapes.", val), 817 | } 818 | (patterns, tilings) 819 | } 820 | 821 | /// Read shape from one of its names 822 | fn add_shape(s: &str, w: usize, tilings: &mut Chooser, patterns: &mut Chooser) { 823 | match &s[..] { 824 | "H" | "hex." | "hexagons" => tilings.push(Tiling::Hexagons, w), 825 | "T" | "tri." | "triangles" => tilings.push(Tiling::Triangles, w), 826 | "H&T" | "hex.&tri." | "hexagons&squares" => tilings.push(Tiling::HexagonsAndTriangles, w), 827 | "S&T" | "squ.&tri." | "squares&triangles" => tilings.push(Tiling::SquaresAndTriangles, w), 828 | "R" | "rho." | "rhombus" => tilings.push(Tiling::Rhombus, w), 829 | "D" | "del." | "delaunay" => tilings.push(Tiling::Delaunay, w), 830 | "P" | "pen." | "pentagons" => tilings.push(Tiling::Pentagons(0), w), 831 | "P1" | "pen.1" | "pentagons-1" => tilings.push(Tiling::Pentagons(1), w), 832 | "P2" | "pen.2" | "pentagons-2" => tilings.push(Tiling::Pentagons(2), w), 833 | "P3" | "pen.3" | "pentagons-3" => tilings.push(Tiling::Pentagons(3), w), 834 | "P4" | "pen.4" | "pentagons-4" => tilings.push(Tiling::Pentagons(4), w), 835 | "P5" | "pen.5" | "pentagons-5" => tilings.push(Tiling::Pentagons(5), w), 836 | "P6" | "pen.6" | "pentagons-6" => tilings.push(Tiling::Pentagons(6), w), 837 | "FC" | "f-cir." | "free-circles" => patterns.push(Pattern::FreeCircles, w), 838 | "FT" | "f-tri." | "free-triangles" => patterns.push(Pattern::FreeTriangles, w), 839 | "FR" | "f-str." | "free-stripes" => patterns.push(Pattern::FreeStripes, w), 840 | "FP" | "f-spi." | "free-spirals" => patterns.push(Pattern::FreeSpirals, w), 841 | "CC" | "c-cir." | "concentric-circles" => patterns.push(Pattern::ConcentricCircles, w), 842 | "PS" | "p-str." | "parallel-stripes" => patterns.push(Pattern::ParallelStripes, w), 843 | "CS" | "c-str." | "crossed-stripes" => patterns.push(Pattern::CrossedStripes, w), 844 | "PW" | "p-wav." | "parallel-waves" => patterns.push(Pattern::ParallelWaves, w), 845 | "PT" | "p-saw." | "parallel-sawteeth" => patterns.push(Pattern::ParallelSawteeth, w), 846 | _ => println!("{} is not recognized as a shape", s), 847 | } 848 | } 849 | 850 | fn choose_theme_shapes( 851 | rng: &mut ThreadRng, 852 | entry: &Option>, 853 | time: usize, 854 | ) -> (String, String, String) { 855 | match entry { 856 | None => (String::from(""), String::from(""), String::from("")), 857 | Some(v) => { 858 | let mut valid = Chooser::new(vec![]); 859 | for e in v { 860 | let markers = e 861 | .span 862 | .as_ref() 863 | .unwrap_or(&"-".to_string()) 864 | .split('-') 865 | .map(String::from) 866 | .collect::>(); 867 | let start = markers 868 | .get(0) 869 | .as_ref() 870 | .unwrap_or(&&String::from("0")) 871 | .parse::() 872 | .unwrap_or(0); 873 | let end = markers 874 | .get(1) 875 | .as_ref() 876 | .unwrap_or(&&String::from("2400")) 877 | .parse::() 878 | .unwrap_or(2400); 879 | if start <= time && time <= end { 880 | valid.push(e, e.distance.unwrap_or(BASE_WEIGHT)); 881 | } 882 | } 883 | match valid.choose(rng) { 884 | None => (String::from(""), String::from(""), String::from("")), 885 | Some(chosen_entry) => { 886 | let chosen_theme = match &chosen_entry.themes { 887 | None => String::from(""), 888 | Some(th) => th 889 | .choose(rng) 890 | .map(String::from) 891 | .unwrap_or_else(|| String::from("")), 892 | }; 893 | let chosen_shapes = match &chosen_entry.shapes { 894 | None => String::from(""), 895 | Some(sh) => sh 896 | .choose(rng) 897 | .map(String::from) 898 | .unwrap_or_else(|| String::from("")), 899 | }; 900 | let line_color = chosen_entry 901 | .line_color 902 | .as_ref() 903 | .map(|s| s.to_string()) 904 | .unwrap_or_else(|| String::from("")); 905 | (chosen_theme, chosen_shapes, line_color) 906 | } 907 | } 908 | } 909 | } 910 | } 911 | 912 | impl ConfigLines { 913 | fn get_settings(&self, tiling: Tiling, colors: &HashMap) -> (f64, Color) { 914 | let (w, c) = match tiling { 915 | Tiling::Hexagons => (self.hex_width, &self.hex_color), 916 | Tiling::Triangles => (self.tri_width, &self.tri_color), 917 | Tiling::HexagonsAndTriangles => (self.hex_and_tri_width, &self.hex_and_tri_color), 918 | Tiling::SquaresAndTriangles => (self.squ_and_tri_width, &self.squ_and_tri_color), 919 | Tiling::Rhombus => (self.rho_width, &self.rho_color), 920 | Tiling::Pentagons(_) => (self.pen_width, &self.pen_color), 921 | Tiling::Delaunay => (self.del_width, &self.del_color), 922 | }; 923 | ( 924 | w.unwrap_or_else(|| self.width.unwrap_or(LINE_WIDTH)), 925 | match c { 926 | Some(c) => color_from_value(&Value::String(c.to_string()), colors).ok(), 927 | None => None, 928 | } 929 | .unwrap_or_else(|| { 930 | match &self.color { 931 | Some(color) => color_from_value(&Value::String(color.to_string()), colors).ok(), 932 | None => None, 933 | } 934 | .unwrap_or(LINE_COLOR) 935 | }), 936 | ) 937 | } 938 | } 939 | 940 | const DEVIATION: usize = 20; 941 | const DISTANCE: usize = 40; 942 | const SIZE: f64 = 15.; 943 | const WIDTH: usize = 1000; 944 | const HEIGHT: usize = 600; 945 | const NB_FREE_CIRCLES: usize = 10; 946 | const NB_FREE_TRIANGLES: usize = 15; 947 | const NB_FREE_STRIPES: usize = 7; 948 | const NB_PARALLEL_STRIPES: usize = 15; 949 | const NB_CONCENTRIC_CIRCLES: usize = 5; 950 | const NB_CROSSED_STRIPES: usize = 10; 951 | const NB_FREE_SPIRALS: usize = 3; 952 | const NB_PARALLEL_WAVES: usize = 15; 953 | const NB_PARALLEL_SAWTEETH: usize = 15; 954 | const VAR_PARALLEL_STRIPES: usize = 15; 955 | const VAR_CROSSED_STRIPES: usize = 10; 956 | const WIDTH_SPIRAL: f64 = 0.3; 957 | const WIDTH_STRIPE: f64 = 0.1; 958 | const WIDTH_WAVE: f64 = 0.3; 959 | const WIDTH_SAWTOOTH: f64 = 0.3; 960 | const TIGHTNESS_SPIRAL: f64 = 0.5; 961 | const NB_DELAUNAY: usize = 1000; 962 | const LINE_WIDTH: f64 = 1.0; 963 | const LINE_COLOR: Color = Color(0, 0, 0); 964 | --------------------------------------------------------------------------------