Stream Art

stream-art

Using svg-clj to create some stream art assets and/or make generative art while streaming.

deps

{:deps
  {io.github.adam-james-v/svg-clj {:mvn/version "0.0.3-SNAPSHOT"}
   hiccup/hiccup                  {:mvn/version "2.0.0-alpha2"}}}

ns

(ns stream-art.draw
 (:require  [svg-clj.utils :as utils]
  [svg-clj.elements :as el]
  [svg-clj.transforms :as tf]
  [svg-clj.composites :as comp :refer [svg]]
  [svg-clj.path :as path]
  [svg-clj.parametric :as p]
  [svg-clj.layout :as lo]
  [reagent.core :as r]))

utils

svg

Make an SVG function. It turns out that you can natively display SVG in emacs (GUI mode, not terminal). This lets us export SVG to a file and immediately display it as a code block result. The svg! fn helps with this.

This is a side-effecting function that writes a file to the same directory as this org file. If you want to control where images are saved, you can change this function and/or the fn calls to save into a directory structure of your choosing.

The file name is returned as a symbol so that org-mode correctly inserts an inline image link to that filename. If you return it as a string, org-mode incorrectly has double quotes around the filename, resulting in no display of the image.

#_(defn svg!
  [svg-data fname]
  (let [svg-data (if (= (first svg-data) :svg)
                   svg-data
                   (svg svg-data))]
    (spit fname (html svg-data))
    (symbol fname)))

time

Time utility is only relevent in a CLJS browser context. This is here as an idea... not sure if the usage pattern is perfect yet. I am basing this off of OpenSCAD's $t special variable which is a parameter running from 0 to 1 based on framerate FPS settings. This is a rough approximation of that where $t runs from 0 to 1 and resets every second.

If you want to update a src block with time, you must @time inside the block.

Then, any randomly generated functions will 're-roll', so there must be some defonce trickery and/or better random generation machinery (eg. use a seed approach, so you can lock in a random config by passing in the seed to the function).

(defonce time (r/atom 0))

(add-watch time :reset
  (fn [key atom old-state new-state]
    (when (<= 100.1 new-state)
      (reset! time 0))))

(js/setTimeout #(swap! time inc) 1)

(def $t (/ @time 100))
(def $st (js/Math.sin (* $t js/Math.PI)))

(defn ease-in-out-circ
  [t]
  (if (< t 0.5)
    (/ (- 1 (js/Math.sqrt (- 1 (js/Math.pow (* 2 t) 2)))) 2)
    (/ (+ (js/Math.sqrt (- 1 (js/Math.pow (+ (* -2 t) 2) 2))) 1) 2)))

(def $cst (ease-in-out-circ $st))

Drawing and Ideas

This is where I build the drawing...

Basic Random Grid Thing

Start the drawing process with a very simple random pattern that looks quite nice. Randomly draw forward or backward slashes along a square grid to make a maze type pattern. Very simple, but still quite pleasing.

Here are some parameters for the drawing.

(def cell-size 16)
(def base-style {:stroke "#83aa9d"
                 :stroke-width 3
                 :stroke-linecap "round"})

And here we can quickly assemble an example.

(def fsl
  (-> (el/line [cell-size 0] [0 cell-size])
      (tf/style base-style)))

(def bsl
  (-> (el/line [0 0] [cell-size cell-size])
      (tf/style base-style)))

(defn rsl []
  (let [idx (rand-int 2)]
    (get [fsl bsl] idx)))

(->
  (lo/distribute-on-pts 
   (repeatedly rsl) 
   (p/rect-grid 20 20 cell-size cell-size))
  svg)

Extending The Idea

I think we can extend this premise using some simple bezier curves to add to the list of curves to draw within each cell.

(def bfsl1
  (-> (path/bezier [cell-size 0]
                   [0 0]
                   [0 cell-size])
      (tf/style base-style)))

(def bbsl1
  (-> (path/bezier [0 0] 
                   [0 cell-size]
                   [cell-size cell-size])
      (tf/style base-style)))

(def bfsl2
  (-> (path/bezier [cell-size 0]
                   [0 0]
                   [cell-size cell-size]
                   [0 cell-size])
      (tf/style base-style)))

(def bbsl2
  (-> (path/bezier [0 0] 
                   [0 cell-size] 
                   [cell-size 0]
                   [cell-size cell-size])
      (tf/style base-style)))

(defn rsl []
  (let [idx (rand-int 6)]
    (get [fsl bsl bfsl1 bbsl1 bfsl2 bbsl2] idx)))

(->
  (lo/distribute-on-pts 
   (repeatedly rsl) 
   (p/rect-grid 20 20 cell-size cell-size))
  svg)

Circle Path Ideas

If I use circle paths, I can make a neat looking ring of glyphs.

(defn circle-pts [n r]
  (let [step (/ 1 n)]
    (map (p/circle r) (range 0 1 step))))
  
(->
  (lo/distribute-on-pts 
   (repeatedly 200 rsl) 
   (concat
    (circle-pts 28 120)
    (circle-pts 28 120)))
  svg)

Gradients

(defn linear-gradient
  [id deg col-a col-b]
  (let [[x1 y1] (utils/rotate-pt-around-center [0 50] deg [50 50])
        [x2 y2] (utils/rotate-pt-around-center [100 50] deg [50 50])]
    [:linearGradient {:id id
                      :x1 (str x1 "%")
                      :y1 (str y1 "%")
                      :x2 (str x2 "%")
                      :y2 (str y2 "%")}
     [:stop {:offset "0%" :stop-color col-a}]
     [:stop {:offset "100%" :stop-color col-b}]]))
@time
[:svg [:defs
  (linear-gradient "grad" (* $t 360) "#1D2B64" "#F8CDDA")
  (linear-gradient "grad1" 3 "slategray" "lavender")
  (linear-gradient "grad2" 3 "slategray" "lavender")]]

Deviating Points Randomly

A new idea... deviate a pt randomly within a radius around itself, or do not deviate according to some deviation chance distribution.

It would be cooler to have a 'deviate along' function where a point deviates some distance within R only along a single vector. This could be used to make a circle with random 'rays' extending from its center. It would look cooler I think.

(def deviate-chance [true true true false false])

(defn- rand-t [] 
  (let [n 10] 
    (/ (inc (rand-int n)) (float n))))

(defn deviate [c ctr pt]
  (let [deviate?
        (get deviate-chance (rand-int (count deviate-chance)))]
    (if deviate?
      (let [ray (p/line ctr pt)]
        (ray ((get [- +] (rand-int 2)) 1 (* (rand-t) c))))
      pt)))

(defn parametric-devious-poly
  [n r]
  (let [pts (circle-pts n r)
        dpts (mapv (partial deviate 0.125 [0 0]) pts)
        lines (map p/line pts dpts)]
    (fn [t]
      (path/polygon (mapv #(% t) lines)))))

(defonce asdf1 (parametric-devious-poly 60 120))
(defonce asdf2 (parametric-devious-poly 50 100))
(defonce asdf3 (parametric-devious-poly 40 80))
(defonce asdf4 (parametric-devious-poly 30 60))
(defonce asdf5 (parametric-devious-poly 20 40))
(defonce asdf6 (parametric-devious-poly 10 20))

@time

(->
  (el/g
   (asdf1 (* (- $cst 0.5) -4))
   (asdf2 (* (- $cst 0.5) 4))
   (asdf3 (* (- $cst 0.5) -3))
   (asdf4 (* (- $cst 0.5) 3))
   (asdf5 (* (- $cst 0.5) -6))
   (asdf6 (* (- $cst 0.5) 6)))
  (tf/style base-style)
  (tf/style {:stroke "url(#grad)"
             :stroke-width 7})
  (tf/translate [150 150])
  (svg 300 300))

Moving Bezier Control Points

Tie Bezier points to the time variables and see if things look cool.

(def asdf (p/circle (+ 10 (* $st (+ $t) 40))))
(def beza
  (-> (tf/merge-paths
       (path/bezier [0 0] 
       	            (utils/v- [20 160] (asdf $t))
        	           (utils/v+ [140 20] (asdf (* 2 $t)))
          	         [200 200])
       (path/polyline [[0 0] [-10 -10] [-10 210] [210 210] [200 200]]))
    (tf/translate [0 0])))

(def bezb
  (-> (tf/merge-paths
       (path/bezier [0 0] 
       	            (utils/v- [20 160] (asdf $t))
        	           (utils/v+ [140 20] (asdf (* 2 $t)))
          	         [200 200])
       (path/polyline [[0 0] [-10 -10] [210 -10] [210 210] [200 200]]))
    (tf/translate [0 0])))

@time
(->
  (el/g
  	(-> beza (tf/style {:fill "url(#grad1)"}))
  	(-> bezb (tf/style {:fill "url(#grad)"})))
  (svg 200 200))

This is a work in progress example

This project is ongoing, but I think is still worth pushing to my blog as an example of how you can 'evolve' your documents.

For something non-critical like this artwork, it's a fun, low-concern way to iterate on some simple ideas. It's actually a lot of fun to play with ideas directly in the webpage output too, though at this time, you still can't save changes in the browser, so just be sure to copy/paste any winning ideas back into your orginal document.

Over time, I think I will make things more ergonomic for this use case, but I am very pleased with how fun and simple it was to mess around with creating some artwork using Clojurescript and SVG in the browser.

Feel free to mess around with the code I've got above. Have a nice day, stay cool, stay healthy.