dependencies
| (this space intentionally left almost blank) | |||||||||||||||
(ns rand-circles.color
(:require [clojure.string :refer [split]]
[rand-circles.util :refer [string-to-float]])) | ||||||||||||||||
Adobe Kuler provides a fast, interactive way to select color schemes. As you explore different options, the url is updated to reflect the colors you've selected. I've pasted and parsed a number of these below. | ||||||||||||||||
Given a string url, return a list of RGB values (themselves 3-element lists.) | (defn parse-kuler-url
[url]
(->> url
(re-find #"rgbvalues=(.*)&swatchOrder")
(#(nth % 1))
(#(split % #","))
(map string-to-float)
(map #(* 255 %))
(partition 3))) | |||||||||||||||
(def kuler-urls ["https://color.adobe.com/create/color-wheel/?base=3&rule=Monochromatic&selected=1&name=My%20Color%20Theme&mode=rgb&rgbvalues=1,1,1,0.007918733758563676,0.13505272703720764,0.16431372549019613,0.10980392156862745,0.6039215686274509,0.7176470588235294,0.31951807228915663,0.8057390548913937,0.9176470588235295,0.029766123316796626,0.5076564321804343,0.6176470588235294,0.020127569099929153,0.34327244461724615,0.41764705882352954&swatchOrder=1,3,2,0,4" "https://color.adobe.com/create/color-wheel/?base=3&rule=Monochromatic&selected=3&name=My%20Color%20Theme&mode=rgb&rgbvalues=0.4066666666666667,0.2596139230884888,0.019598393574297206,0.8833333333333333,0.5990896747929055,0.1351548269581056,0.44235294117647045,0.31120321991081035,0.09714417531718567,0.86,0.5490196078431373,0.04144578313253016,0.6599999999999999,0.4213406292747604,0.03180722891566268,1,1,1&swatchOrder=1,3,2,0,4" "https://color.adobe.com/create/color-wheel/?base=3&rule=Monochromatic&selected=3&name=My%20Color%20Theme&mode=rgb&rgbvalues=0.18338535414172807,0.3152941176470589,0.017001229407425628,0.4973372027963519,0.7919607843137255,0.1257115015602658,0.23074661667833857,0.35098039215686266,0.07908882712073723,0.4470588235294118,0.7686274509803922,0.04144578313253016,0.33073229291729556,0.5686274509803921,0.030661421194984023&swatchOrder=1,3,2,0,4" "https://color.adobe.com/create/color-wheel/?base=3&rule=Complementary&selected=4&name=My%20Color%20Theme&mode=rgb&rgbvalues=0.4686274509803922,0.31902604331445644,0.012634620113105488,1,0.7224200037850999,0.1539218096877305,0,0.17937800070405646,0.4686274509803922,0.7686274509803922,0.5300548380469997,0.04144578313253013,0.2627450980392157,0.45638327453134514,0.7686274509803922&swatchOrder=1,3,2,0,4" "https://color.adobe.com/create/color-wheel/?base=3&rule=Triad&selected=2&name=My%20Color%20Theme&mode=rgb&rgbvalues=0.318399152221312,0.4686274509803922,0.07213198532425019,0.1242440751400962,0.3244275613327357,0.8071895424836601,0.5686274509803921,0.3003751020440592,0.24234924695801538,0.4931053471682004,0.7686274509803922,0.04144578313253013,0.4686274509803922,0.08485249869608935,0.0018378676771913438&swatchOrder=1,3,2,0,4"]) | ||||||||||||||||
(def colors (mapcat parse-kuler-url kuler-urls)) | ||||||||||||||||
(ns rand-circles.core
(:require [incanter.interpolation :refer [interpolate-parametric]]
[clojure.algo.generic.functor :refer [fmap]]
[clojure.string :refer [join split]]
[rand-circles.color]
[rand-circles.util :refer [linspace
zip
expt-list
reciprocal
rands-with-amplitude
sum-funcs
hex-to-rgb
zip-keys
rescale
string-to-float]])
(:gen-class)) | ||||||||||||||||
Random walks with Perlin Noise | ||||||||||||||||
Random walks provide a simple way to create natural-looking shapes and lines. Perlin noise is one method that allows you to finely tune how rough you'd like your data to be. | ||||||||||||||||
First, we'll create a Perlin noise generator, then use it to control the position, color, and size of several shapes. | ||||||||||||||||
Let's get started! | ||||||||||||||||
How Perlin noise works. | ||||||||||||||||
Perlin noise is smoother and more natural than a list of random values. It achieves this in the following way: | ||||||||||||||||
| ||||||||||||||||
Implementing a Perlin noise generator | ||||||||||||||||
The function The second argument, | ||||||||||||||||
To start off, Since we now know how many points to pick on each iteration, and the range we should pick from, we can get a bunch of points using | (defn get-perlin-func [freq levels]
"Return a function, valid over domain [0,1] which produces Perlin noise around range [-1,1]."
(let [c (expt-list 2 levels)
frequencies (map (partial * freq) c)
amplitudes (map reciprocal c)]
(->> [frequencies amplitudes]
(apply map rands-with-amplitude)
(map #(interpolate-parametric % :cubic))
(apply sum-funcs)))) | |||||||||||||||
Return path through waypoints with given roughness, at each value of x. Inputs: waypoints - collection of scalars or collections (of scalars) roughness - scalar between 0 and 1 x - collection of scalars. Now that we've got a Perlin noise generator, we can use it to control some parameter, like position. Since the output of the generator is around the range [-1,1], we'll need to rescale it to whatever range we need. | (defn get-rough-path
[roughness waypoints x]
(->> x
(map (comp (interpolate-parametric waypoints :linear)
(partial rescale [-1.2 1.2] [0 1])
(get-perlin-func (rescale [0 1] [5 50] roughness) 4))))) | |||||||||||||||
Create a sequence of hashmaps representing circles where each parameter is generated by a random walk. | (defn get-random-walking-circles
[config]
(->> [:color :opacity :radius :x :y]
(select-keys config)
(fmap #(get-rough-path (:roughness config)
%
(linspace 0 1 (:n-points config))))
zip-keys)) | |||||||||||||||
Create an string specifying a circle in SVG, given a hashmap with the appropriate properties. | (defn make-svg-circle
[circle]
(let [names {:radius "r"
:color "fill"
:opacity "fill-opacity"
:x "cx"
:y "cy"}
format-color (fn [c] (update-in c [:color] #(str "rgb(" (join "," %) ")")))]
(->> circle
format-color
(map (fn [[k v]] (str (k names) "=" "\"" v "\"")))
(join " ")
(#(str "<circle " % "/>"))))) | |||||||||||||||
Create SVG ;;;;;;;;;;;;;;;;;;; | ||||||||||||||||
(def config
{:x [300 1300]
:y [200 700]
:roughness 0.9
:n-points 10000
:color rand-circles.color/colors
:opacity [0.05 0.4]
:radius [0 30]}) | ||||||||||||||||
(def f "images/output.svg") | ||||||||||||||||
(spit f (->> config
get-random-walking-circles
(map make-svg-circle)
(join "\n")
(#(str "<svg width=\"1600\" height=\"900\"> <g id=\"layer1\"> " % " </g></svg>")))) | ||||||||||||||||
(defn -main []) | ||||||||||||||||
(ns rand-circles.util
(:require [clojure.math.numeric-tower :as math]
[incanter.interpolation :refer [interpolate]])) | ||||||||||||||||
(defn negate [x] "Return -x." (- 0 x)) | ||||||||||||||||
(defn reciprocal [x] "Return 1/x." (/ 1.0 x)) | ||||||||||||||||
(defn expt-list [base n]
"Return list of length n, starting with base^0 and ending with base^(n-1)."
(->> (range n)
(map (partial math/expt base)))) | ||||||||||||||||
(defn sum [coll] "Return sum of values in coll." (reduce + coll)) | ||||||||||||||||
(defn zip [& seqs] "Group elements of provided sequences into vectors by index." (apply map vector seqs)) | ||||||||||||||||
(defn linspace [a b n]
"Return list of n equally-spaced elements,
with a as first and b as last."
(let [span (- b a)
interval (/ span (dec n))]
(->> (range n)
(map #(+ a (* interval %)))
(map float)))) | ||||||||||||||||
(defn rand-in-range [[low high]]
"Return random float in between low and high."
(let [range-val (- high low)]
(+ low (rand range-val)))) | ||||||||||||||||
(defn rands-in-range [n [low high]] "Return n random floats between low and high." (repeatedly n #(rand-in-range [low high]))) | ||||||||||||||||
(defn rands-with-amplitude [n amplitude] "Return n random floats between +amplitude and -amplitude." (rands-in-range n [(negate amplitude) amplitude])) | ||||||||||||||||
(defn sum-funcs [& funcs]
"Return function where output is sum of all outputs from provided functions."
(->> funcs
(apply juxt)
(comp sum))) | ||||||||||||||||
Partition string s into substrings of length n. Remainders are ignored. | (defn partition-string
[n s]
(->> s
(partition n)
(map (partial apply str)))) | |||||||||||||||
Convert hex string into base-10 integer. | (defn hex-to-decimal [hex-string] (Integer/parseInt hex-string 16)) | |||||||||||||||
Return rgb-value of hex color as 3-element list. | (defn hex-to-rgb
[hex-string]
(->> hex-string
(partition-string 2)
(map hex-to-decimal))) | |||||||||||||||
Return transpose of a list-of-lists. | (defn transpose [x] (apply map list x)) | |||||||||||||||
Return collection of hashmaps given a hashmap of collections. | (defn zip-keys
[x]
(->> (vals x)
transpose
(map (partial zipmap (keys x))))) | |||||||||||||||
Given an x in input-range, remap to output-range. | (defn rescale
[input-range output-range x]
(let [[l1 h1] input-range
[l2 h2] output-range
points [[l1 l2] [h1 h2]]]
((interpolate points :linear) x))) | |||||||||||||||
Convert string to a float. | (defn string-to-float [x] (Float. x)) | |||||||||||||||