rand-circles

0.1.0-SNAPSHOT


Generative art: create a random-walk.

dependencies

org.clojure/clojure
1.6.0
org.clojure/math.numeric-tower
0.0.4
incanter
1.9.0
incanter/incanter-core
1.9.0
org.clojure/algo.generic
0.1.2



(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:

  1. Pick a few random points in some range (i.e. between -1 and 1)
  2. Spread these points evenly over some domain (set of x-values)
  3. Interpolate them, producing a smooth function that intersects each randomly-generated point.
  4. Repeat the process, but choose more points, and in a smaller range (i.e. between -0.5 and 0.5)
  5. Now, add the functions together. This superimposes the rougher function on the first, smoother function.
  6. You can repeat this process of making new functions and summing them all up as many times as you like, though each function will change the output of the noise generator less and less.

Implementing a Perlin noise generator

The function get-perlin-func returns a function defined over the domain [0,1] which will output a random value around the range [-1,1]. The first argument, freq, defines how many random points to select for the first step explained above. The higher the number, the rougher the noise. If the number is below 3, however, there won't be enough points to fit a cubic interpolator, and the function will fail.

The second argument, levels, defines how many times to go through the steps above. If levels is one, then only one set of random points will be generated. If levels is two, then two sets of random points will be generated, interpolated, and summed.

To start off, c is a list of the form [1 2 4 8 ...]. We'll use it to select n random points for our first run, then 2n points for the second, and so on, as you can see in the definition of frequencies. c is also used to define the amplitude, or range of random numbers to pick from on each iteration. By taking the reciprocal (1/x) of c, we get the sequence, [1 1/2 1/4 ...], which makes the random numbers smaller on each iteration.

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 rands-with-amplitude. After running it, we get a list of lists, where each sublist contains an increasingly large number of random values. The next step is to create a cubic interpolation function for each of these lists of values with get-cubic-interpolator. When that's finished, we create a new function that returns the sum of all the interpolators we just made with `sum-funcs'.

(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))