Generative Images with SVG.js
I recently came across some simple but fun generative drawings posted by FrederikVanhoutte.
The algorithm governing their creation is quite simple:
- Create a 10x10 square grid
- Roll a 10-sided die to pick a starting location along the left-hand side of the grid (
y1
) - Roll a 10-sided die to determine how long to draw a line toward the right-hand side of the grid (
x
) - Role a 10-sided die to determine the ending location along the right-hand side of the grid (
y2
) - Draw three, connected lines with these vertices:
(0, y1), (x,y1), (x,y2), (10,y2)
There are two additional aesthetic rules that govern the appearance of the lines in the drawing:
- If two line segments would overlap, offset the second until it does not overlap any prior segments
- If a line crosses another, create a gap at their intersection to keep them from touching
Frederik’s postings show pen and paper drawings he created using the above rules, a straight-edge, and a 10-sided die. I wanted to see the results would look like when imagined on a computer screen.
A brief search revealed svg.js, a nice library for creating scalable vector graphics (SVG) directly in a browser page. The docs leverage JSFiddle to do this, and that is the approach I took as well. The code for my generative version is here.[1] Press the “Run” button in the upper left-hand corner of your browser to generate new images.
Comments on the Code
I am not a strong Javascript programmer, but I think the code is fairly straightforward. I also think it does a good job replicating what Frederik achieved with pen and paper. Note that I deviated slightly from above when calculating the x
value: instead of a 10-sided die, I roll one with 9 sides.
The code starts out with some configuration settings that affect the appearance of the generated image. Next are two function definitions xmur3
and sfc32
which I shamelessly copied from a stackoverflow posting. These two functions allow me to generate random values much like Math.random
but with control of the initial seed values for the random sequence. This lets me recreate any image as long as I know the original seed values. For simplicity, I use the value from Date()
which returns a string showing the current date and time.
After the random routine setup, I create a new SVG image and attach it to the “canvas” div
element on the web page. The routine offsets
takes in three values (y1
, x
, y2
) and returns non-overlapping values which drawFigure
uses to create the vertices of the lines it draws. To achieve the gap effect mentioned above when two lines intersect, I first draw the lines with a wide pen in white to erase any intersections, and then I draw with a narrower pen in black for the final lines. Finally, I create new SVG image that shows the random seed value just in case I wish to recreate the image in the future.
Code
// Replicate 'System 190304a-c "Connections"'' by Frederik Vanhoutte
// https://twitter.com/wblut/status/1102557824192835584
const lineCount = 14
const lineWidth = 0.04
const gap = 0.02
const lineWidthAndGap = lineWidth + gap
const gridAttr = {
width: 0.01,
color: '#eee'
}
const pathAttr = {
width: lineWidth + gap * 2,
color: "#fff"
}
const lineAttr = {
width: lineWidth,
color: "#000"
}
// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript
function xmur3(str) {
for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++)
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
h = h << 13 | h >>> 19;
return function() {
h = Math.imul(h ^ h >>> 16, 2246822507);
h = Math.imul(h ^ h >>> 13, 3266489909);
return (h ^= h >>> 16) >>> 0;
}
}
// Obtain a random number generator with given initial state
function sfc32(a, b, c, d) {
return function() {
a >>>= 0;
b >>>= 0;
c >>>= 0;
d >>>= 0;
var t = (a + b) | 0;
a = b ^ b >>> 9;
b = c + (c << 3) | 0;
c = (c << 21 | c >>> 11);
d = d + 1 | 0;
t = t + d | 0;
c = c + t | 0;
return (t >>> 0) / 4294967296;
}
}
// Set to constant string value to recreate the same image
const randSeed = Date() // "Tue Mar 05 2019 14:03:31 GMT+0100 (CET)"
const seedGen = xmur3(randSeed)
const randGen = sfc32(seedGen(), seedGen(), seedGen(), seedGen())
var canvas = SVG().addTo('#canvas')
canvas.size('100%', '100%')
canvas.viewbox(-1, -1, 11, 11)
var xpaths = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
var ypaths = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
// Offset given values based on previous lines
function offsets(y1, x, y2) {
// First calculate offsets
const ry1 = ypaths[y1]
const ry2 = ypaths[y2]
const rx = xpaths[x]
const yo1 = Math.max(...ry1.slice(0, x))
const yo2 = Math.max(...ry2.slice(x, 10))
const rxs = y2 > y1 ? rx.slice(y1, y2) : rx.slice(y2, y1)
const xo = Math.max(...rxs)
// Now update values for future offset calculations
for (var i = 0; i < x; ++i) {
ry1[i] = yo1 + lineWidthAndGap
}
for (i = x; i < 10; ++i) {
ry2[i] = yo2 + lineWidthAndGap
}
if (y2 > y1) {
for (i = y1; i < y2; ++i) {
rx[i] = xo + lineWidthAndGap
}
} else {
for (i = y2; i < y1; ++i) {
rx[i] = xo + lineWidthAndGap
}
}
return [y1 + yo1, x + xo, y2 + yo2]
}
// Generate random integer in range [x, y)
function rint(x, y) {
return Math.floor(randGen() * (y - x + 1)) + x
}
// Draw faint graph paper lines
for (var i = 0; i < 10; ++i) {
canvas.line(i, 0, i, 9).stroke(gridAttr)
canvas.line(0, i, 9, i).stroke(gridAttr)
}
function drawFigure() {
const y1i = rint(0, 9)
const y2i = rint(0, 9)
if (y2i == y1i) return drawFigure()
const yxy = offsets(y1i, rint(1, 8), y2i)
const y1 = yxy[0]
const x = yxy[1]
const y2 = yxy[2]
const pts = [0, y1, x, y1, x, y2, 9, y2]
canvas.polyline(pts).fill('none').stroke(pathAttr)
canvas.polyline(pts).fill('none').stroke(lineAttr)
}
for (var j = 0; j < lineCount; ++j) {
drawFigure()
}
canvas = SVG().addTo('#canvas')
canvas.size('100%', '100%')
canvas.text('Seed: ' + randSeed).font({
family: 'Arial',
size: 14
})