Mathias Brandewinder on .NET, F#, VSTO and Excel development, and quantitative analysis / machine learning.
20. December 2014 01:55

This post is December 20th' part of the English F# Advent #fsAdvent series; make sure to also check out the Japanese series, which also packs the awesome!

As I was going around Paris the other day, I ended up in the Concorde metro station. Instead of the standard issue white tiles, this station is decorated with the French constitution, rendered as a mosaic of letters.

My mind started wandering, and by some weird association, it reminded me of Calligrammes, a collection of poems by Guillaume Apollinaire, where words are arranged on the page to depict images that compliment the text itself.

After some more drifting, I started wondering if I could use this as an inspiration for some playful F# fun. How about taking a piece of text, an image, and fusing them into one?

There are many ways one could approach this; being rather lazy, I thought a reasonably simple direction would be to decompose the original image into dark and light blocks, and fill them with the desired text. As simple as it may sound, the task is not entirely trivial. First, we need to decide on an appropriate threshold to separate "dark" and "light" areas on the image, to get a contrast good enough to recognize the image rendered in black & white. Then, we also have to resize the image appropriately into a new grid, where the characters from the original text fit the mapped dark area as closely as possible.

Note: I don't think the warning "don't put this in production, kids" is useful, unless someone thinks there is a market for Calligramme as a Service. However, I'll say this: this is me on "vacation hacking fun" mode, so yes, there are quite probably flaws in that code. I put it up as a Gist here - flame away, or tell me how to make it better on Twitter ;)

## Separating dark and light

So how could we go about splitting an image into dark and light pixels? First, we can using the color brightness from the System.Drawing namespace to determine how light the color of an individual pixel is:

open System.Drawing

let brightness (c:Color) = c.GetBrightness ()

let pixels (bmp:Bitmap) =
seq { for x in 0 .. bmp.Width - 1 do
for y in 0 .. bmp.Height - 1 ->
(x,y) |> bmp.GetPixel }

We still need to decide what boundary to use to separate the image between dark and light. What we want in the end is an image which is reasonably balanced, that is, it should be neither overwhelmingly dark or light. A simple way to enforce that is to arbitrarily constrain one third of the pixels at least to be either dark or light. Then, we want a boundary value that is as clear cut as possible, for instance by finding a value with a large brightness change margin. Let's do that:

let breakpoint (bmp:Bitmap) =
let pixelsCount = bmp.Width * bmp.Height
let oneThird = pixelsCount / 3
let pixs = pixels bmp
let threshold =
pixs
|> Seq.map brightness
|> Seq.sort
|> Seq.pairwise
|> Seq.skip oneThird
|> Seq.take oneThird
|> Seq.maxBy (fun (b0,b1) -> b1 - b0)
|> snd
let darkPixels =
pixs
|> Seq.map brightness
|> Seq.filter ((>) threshold)
|> Seq.length
(threshold,darkPixels)

We iterate over every pixel, sort them by brightness, retain only the middle third, and look for the largest brightness increase. Done - breakpoint returns both the threshold value (the lightness level which decides whether a pixel will be classified as dark or light), as well as how many pixels will be marked as dark.

## Resizing

Now that we have a boundary value, and know how many pixels will be marked as dark, we need to determine the size of the grid where our text will be mapped. Ignoring for a moment rounding issues, let's figure out a reasonable size for our grid.

First, how many characters do we need? We know the number of dark pixels in the original image - and in our target image, we want the same ratio of text to white space, so the total number of characters we'll want in our final image will be roughly total chars ~ text length * dark pixels / (width * height).

Then, what should the width of the target image, in characters? First, we want [1] target width * target height ~ total chars. Then, ideally, the proportions of the target image should be similar to the original image, so target width / target height ~ width / height, which gives us target height ~ target width * height / width. Substituting in [1] gives us target width * target width * height / width ~ total chars, which simplifies to target width ~ sqrt (total chars * width / height).

Translating this to code, conveniently ignoring all the rounding issues, we get:

let sizeFor (bmp:Bitmap) (text:string) darkPixels =
let width,height = bmp.Width, bmp.Height
let pixels = width * height
let textLength = text.Length
let chars = textLength * pixels / darkPixels
let w = (chars * width / height) |> float |> sqrt |> int
let h = (w * height) / width
(w,h)

## Rendering

Good, now we are about ready to get down to business. We have an original image, a threshold to determine which pixels to consider dark or light, and a "target grid" of known width and height. What we need now is to map every cell of our final grid to the original image, decide whether it should be dark or light, and if dark, write a character from our text.

Ugh. More approximation ahead. At that point, there is no chance that the cells from our target grid map the pixels from the original image one to one. What should we do? This is my Christmas vacation time, a time of rest and peace, so what we will do is be lazy again. For each cell in the target grid, we will retrieve the pixels that it overlaps on the original image, and simply average out their brightness, not even bothering with a weighted average based on their overlap surface. As other lazy people before me nicely put it, "we'll leave that as an exercise to the reader".

Anyways, here is the result, a mapping function that returns the coordinates of the pixels intersected by a cell, as well as a reducer, averaging the aforementioned pixels by brightness, and a somewhat un-necessary function that transforms the original image in a 2D array of booleans, marking where a letter should go (I mainly created it because I had a hard time keeping track of rows and columns):

let mappedPixels (bmp:Bitmap) (width,height) (x,y) =

let wScale = float bmp.Width / float width
let hScale = float bmp.Height / float height

let loCol = int (wScale * float x)
let hiCol =
int (wScale * float (x + 1)) - 1
|> min (bmp.Width - 1)
let loRow = int (hScale * float y)
let hiRow =
int (hScale * float (y + 1)) - 1
|> min (bmp.Width - 1)

seq { for col in loCol .. hiCol do
for row in loRow .. hiRow -> (col,row) }

let reducer (img:Bitmap) pixs =
pixs
|> Seq.map img.GetPixel
|> Seq.averageBy brightness

let simplified (bmp:Bitmap) (width,height) threshold =

let map = mappedPixels bmp (width,height)
let reduce = reducer bmp
let isDark value = value < threshold

let hasLetter = map >> reduce >> isDark

Array2D.init width height (fun col row ->
(col,row) |> hasLetter)

Almost there - wrap this with 2 functions, applyTo to transform the text into a sequence, and rebuild, to recreate the final string function:

let applyTo (bmp:Bitmap) (width,height) threshold (text:string) =

let chars = text |> Seq.toList
let image = simplified bmp (width,height) threshold

let nextPosition (col,row) =
match (col < width - 1) with
| true -> (col+1,row)
| false -> (0,row+1)

(chars,(0,0))
|> Seq.unfold (fun (cs,(col,row)) ->
let next = nextPosition (col,row)
match cs with
| [] -> Some(' ',(cs,next))
| c::tail ->
if image.[col,row]
then
Some(c,(tail,next))
else Some(' ',(cs,next)))

let rebuild (width,height) (data:char seq) =
seq { for row in 0 .. height - 1 ->
data
|> Seq.map string
|> Seq.skip (row * width)
|> Seq.take width
|> Seq.toArray
|> (String.concat "") }
|> (String.concat "\n")

## Trying it out

Let's test this out, using the F# Software Foundation logo as an image, and the following text, from fsharp.org, as a filler:

F# is a mature, open source, cross-platform, functional-first programming language. It empowers users and organizations to tackle complex computing problems with simple, maintainable and robust code.

Run this through the grinder…

let path = @"c:/users/mathias/pictures/fsharp-logo.jpg"
let bmp = new Bitmap(path)

let text = """F# is // snipped // """

let threshold,darkPixels = breakpoint bmp
let width,height = sizeFor bmp text darkPixels

text
|> applyTo bmp (width,height) threshold
|> rebuild (width,height)

… and we get the following:


F#
is
a matu
re, open
source, c
ross-platfor
m, fun  ctiona
l-firs t   progr
amming  l   anguag
e. It  emp    owers
users  and      organi
zation s to     tackle
compl ex    computi
ng pro bl  ems wit
h simp l e, main
tainab le and
robust code.      

Not too bad! The general shape of the logo is fairly recognizable, with some happy accidents, like for instance isolating "fun" in "functional". However, quite a bit of space has been left empty, most likely because of the multiple approximations we did along the way.

## Improved resizing

Let's face it, I do have some obsessive-compulsive behaviors. As lazy as I feel during this holiday break, I can't let go of this sloppy sizing issue. We can't guarantee a perfect fit (there might simply not be one), but maybe we can do a bit better than our initial sizing guess. Let's write a mini solver, a recursive function that will iteratively attempt to improve the fit.

Given a current size and count of dark cells, if the text is too long to fit, the solver will simply expand the target grid size, adding one row or one column, picking the one that keeps the grid horizonal/vertical proportions closest to the image. If the text fits better in the new solution, keep searching, otherwise, done (similarly, reduce the size if the text is too short to fit).

For the sake of brevity, I won't include the solver code here in the post. If you are interested, you can find it in the gist here.

Below are the results of the original and shiny new code, which I ran on a slightly longer bit of text.

Before:


F#is
amatur
eopenso
urcecross
-platformfun
ctional-firstp
rogramminglangua
geItempowersusersa
ndorganiz  ationstot
acklecomp    lexcomput
ingproble ms   withsimpl
emaintain abl    eandrobus
tcodeF#ru nson     LinuxMacO
SXAndroid iOSWi      ndowsGPUs
andbrowse rsItis       freetouse
andisope nsourc       eunderanO
SI-approv edlic      enseF#isu
sedinawid eran     geofappli
cationar eas   andissuppo
rtedbybo th   anactive
opencommu    nityandin
aniesprovidingpr
ofessionaltool
s                       

… and after:

               F #
is am
atu reo
pens ourc
ecros s-pla
tformf unctio
nal-fir stprogr
ammingla nguageIt
empowers   usersand
organiza t   ionstota
cklecomp le    xcomputi
ngproble msw     ithsimpl
emaintai nabl      eandrobu
stcodeF# runso       nLinuxMac
OSXAndro  idiOS       WindowsGP
Usandbro wsers      Itisfreet
ouseandi sope     nsourceun
deranOSI -ap    provedlic
enseF#is us   edinawide
rangeofa p  plication
areasand  issupport
edbyboth anactive
opencom munitya
ndindu stry-l
anie spro
vid ing
pr of
e s               

We still have a small mismatch, but the fit is much better.

And this concludes our F# Advent post! This was a rather useless exercise, but then, the holidays are about fun rather than productivity. I had fun doing this, and hope you had some fun reading it. In the meanwhile, I wish you all a holiday period full of fun and happiness, and… see you in 2015! And, as always, you can ping me on twitter if you have comments or questions. Cheers!

16. March 2013 10:04

Mondrian is one of those modern painters whose work everyone recognizes, even though few people will quote his name. He also happens to be one of my favorite artists – in spite of their simple geometric structure, I find his pieces strangely beautiful:

“Composition II in Red, Blue, and Yellow”, from Wikipedia

I have been hard at work on some pretty dry stuff lately, and needed a bit of a change of pace, and ended up spending a couple of hours coding a simple Mondrianizer in F#: give it a picture, and it will transform it into something “in the style of Mondrian”.

For instance, starting from my Twitter avatar, here is what the Mondrianizer produces:

This was strictly quick-and-dirty hackery, so the code is not my best by any stretch of the imagination, but I was rather pleased by the results – you can find the current version of the Mondrianizer here on GitHub.

More...

25. March 2012 10:53

In a previous post, I looked at creating a Sierpinski triangle using F# and WPF. One of the pieces I was not too happy about was the function I used to transform a Triangle into a next generation triangle:

type Point = { X:float; Y:float }
type Triangle = { A:Point; B:Point; C:Point }

let transform (p1, p2, p3) =
let x1 = p1.X + 0.5 * (p2.X - p1.X) + 0.5 * (p3.X - p1.X)
let y1 = p1.Y + 0.5 * (p2.Y - p1.Y) + 0.5 * (p3.Y - p1.Y)
let x2 = p1.X + 1.0 * (p2.X - p1.X) + 0.5 * (p3.X - p1.X)
let y2 = p1.Y + 1.0 * (p2.Y - p1.Y) + 0.5 * (p3.Y - p1.Y)
let x3 = p1.X + 0.5 * (p2.X - p1.X) + 1.0 * (p3.X - p1.X)
let y3 = p1.Y + 0.5 * (p2.Y - p1.Y) + 1.0 * (p3.Y - p1.Y)
{ A = { X = x1; Y = y1 }; B = { X = x2; Y = y2 }; C= { X = x3; Y = y3 }}

Per se, there is nothing wrong with the transform function: it takes 3 points (the triangle corners), and returns a new Triangle. However, what is being “done” to the triangle is not very expressive – and the code looks rather ugly, with clear duplication (the exact same operation is repeated on the X and Y coordinates of every point).

Bringing back blurry memories from past geometry classes, it seems we are missing the notion of a Vector. What we are doing here is taking corner p1 of the Triangle, and adding a linear combinations of the edges p1, p2 and p1, p3 to it, which can be seen as 2 Vectors (p2 – p1) and (p3 – p1). Restated that way, here is what the transform function is really doing:

A –> A + 0.5 x AB + 0.5 x AC

A –> A + 1.0 x AB + 0.5 AC

A –> A + 0.5 x AB + 1.0 x AC

In graphical form, the first transformation can be represented as follows:

In order to achieve this, we need to define a few elements: a Vector, obviously, a way to create a Vector from two Points, to add Vectors, to scale a Vector by a scalar, and to translate a Point by a Vector. Let’s do it:

type Vector =
{ dX:float; dY:float }
static member (+) (v1, v2) = { dX = v1.dX + v2.dX; dY = v1.dY + v2.dY }
static member (*) (sc, v) = { dX = sc * v.dX; dY = sc * v.dY }

type Point =
{ X:float; Y:float }
static member (+) (p, v) = { X = p.X + v.dX; Y = p.Y + v.dY }
static member (-) (p2, p1) = { dX = p2.X - p1.X; dY = p2.Y - p1.Y }

type Triangle = { A:Point; B:Point; C:Point }

Thanks to operators overloading, the transform function can now be re-phrased in a much more palatable way:

let transform (p1:Point, p2, p3) =
let a = p1 + 0.5 * (p2 - p1) + 0.5 * (p3 - p1)
let b = p1 + 1.0 * (p2 - p1) + 0.5 * (p3 - p1)
let c = p1 + 0.5 * (p2 - p1) + 1.0 * (p3 - p1)
{ A = a; B = b; C = c }

… and we are done. The code (posted on fsSnip.net) works exactly as before, but it’s way clearer.

It can also be tweaked more easily now. I got curious about what would happen if slightly different transformations were applied, and the results can be pretty fun. For instance, with a minor modification of the transform function…

let transform (p1:Point, p2, p3) =
let a = p1 + 0.55 * (p2 - p1) + 0.5 * (p3 - p1)
let b = p1 + 1.05 * (p2 - p1) + 0.45 * (p3 - p1)
let c = p1 + 0.5 * (p2 - p1) + 0.95 * (p3 - p1)
{ A = a; B = b; C = c }

… we get the following, bloated “Sierpinski triangle”:

Add a bit of transparency, some more tweaks of the linear combinations,

let transform (p1:Point, p2, p3) =
let a = p1 + 0.3 * (p2 - p1) + 0.6 * (p3 - p1)
let b = p1 + 0.8 * (p2 - p1) + 0.3 * (p3 - p1)
let c = p1 + 0.6 * (p2 - p1) + 1.1 * (p3 - p1)
{ A = a; B = b; C = c }

and things get much wilder:

I don’t think these are really Sierpinski triangles any more, but I had lots of fun playing with this, and figured someone else might enjoy it, too… If you find a nice new combination, post it in the comments!

Source code: fsSnip.net

16. March 2012 08:33

In my last post, I looked into drawing a Sierpinski triangle using F# and WinForms, and noted that the rendering wasn’t too smooth – so I converted it to WPF, to see if the result would be any better, and it is. In the process, I discovered John Liao’s blog, which contains some F# + WPF code examples I found very useful. I posted the code below, as well as on FsSnip. The differences with the WinForms code are minimal, I’ll let the interested reader figure that part out!

One thing I noticed is that the starting point of the Sierpinski sequence is a single triangle – but nothing would prevent a curious user to initialize the sequence with multiple triangles. And while at it, why not use WPF Brush opacity to create semi-transparent triangles, and see how their superposition looks like?

We just change the Brush Color and Opacity, and add a second triangle to the root sequence…

let brush = new SolidColorBrush(Colors.DarkBlue)
brush.Opacity <- 0.6
let renderTriangle = render canvas brush

let triangle =
let p1 = { X = 190.0; Y = 170.0 }
let p2 = { X = 410.0; Y = 210.0}
let p3 = { X = 220.0; Y = 360.0}
{ A = p1; B = p2; C = p3 }

let triangle2 =
let p1 = { X = 290.0; Y = 170.0 }
let p2 = { X = 510.0; Y = 210.0}
let p3 = { X = 320.0; Y = 360.0}
{ A = p1; B = p2; C = p3 }

let root = seq { yield triangle; yield triangle2 }

… and here we go:

Granted, it’s pretty useless, but I thought it looked rather nice!

As an aside, here is something I noted when working in F#: I often end up looking at the code, thinking “can I use this to do something I didn’t think about when I wrote it”? In C#, I tend to think in terms of restrictions: write Components, with a “containment” approach – figure out what the component should do, and enforce safety by constraining the inputs/outputs via an interface. By contrast, because of type inference and the fact that a function doesn’t require an “owner” (it is typically not a member of a class), I find myself less “mentally conditioned”, and instead of a world of IWidgets and ISprockets, I simply see functions that transform elements, and wonder what else they could apply to.

The case we saw here was trivial, but pretty much from the moment I wrote that code, I have been mulling over other extensions. What is the transform function really doing, and what other functions could I replace it with? generateFrom is simply permuting the triangle corners and applying the same transformation – could I generalize this to an arbitrary sequence and write Sierpinski Polygons? Could I even apply it to something that has nothing to do with geometry?

// Requires reference to
// PresentationCore, PresentationFramework,
// System.Windows.Presentation, System.Xaml, WindowsBase

open System
open System.Windows
open System.Windows.Media
open System.Windows.Shapes
open System.Windows.Controls

type Point = { X:float; Y:float }
type Triangle = { A:Point; B:Point; C:Point }

let transform (p1, p2, p3) =
let x1 = p1.X + 0.5 * (p2.X - p1.X) + 0.5 * (p3.X - p1.X)
let y1 = p1.Y + 0.5 * (p2.Y - p1.Y) + 0.5 * (p3.Y - p1.Y)
let x2 = p1.X + 1.0 * (p2.X - p1.X) + 0.5 * (p3.X - p1.X)
let y2 = p1.Y + 1.0 * (p2.Y - p1.Y) + 0.5 * (p3.Y - p1.Y)
let x3 = p1.X + 0.5 * (p2.X - p1.X) + 1.0 * (p3.X - p1.X)
let y3 = p1.Y + 0.5 * (p2.Y - p1.Y) + 1.0 * (p3.Y - p1.Y)
{ A = { X = x1; Y = y1 }; B = { X = x2; Y = y2 }; C= { X = x3; Y = y3 }}

let generateFrom triangle = seq {
yield transform (triangle.A, triangle.B, triangle.C)
yield transform (triangle.B, triangle.C, triangle.A)
yield transform (triangle.C, triangle.A, triangle.B)
}

let nextGeneration triangles =
Seq.collect generateFrom triangles

let render (target:Canvas) (brush:Brush) triangle =
let points = new PointCollection()
let polygon = new Polygon()
polygon.Points <- points
polygon.Fill <- brush

let win = new Window()
let canvas = new Canvas()
canvas.Background <- Brushes.White
let brush = new SolidColorBrush(Colors.Black)
brush.Opacity <- 1.0
let renderTriangle = render canvas brush

let triangle =
let p1 = { X = 190.0; Y = 170.0 }
let p2 = { X = 410.0; Y = 210.0}
let p3 = { X = 220.0; Y = 360.0}
{ A = p1; B = p2; C = p3 }

let root = seq { yield triangle }
let generations =
Seq.unfold (fun state -> Some(state, (nextGeneration state))) root
|> Seq.take 7
Seq.iter (fun gen -> Seq.iter renderTriangle gen) generations

win.Content <- canvas
win.Show()

do
let app =  new Application() in
app.Run() |> ignore
11. March 2012 11:29

I am midway through the highly-recommended “Real-world functional programming: with examples in C# and F#”, which inspired me to play with graphics using F# and WinForms (hadn’t touched that one in a long, long time), and I figured it would be fun to try generating a Sierpinski Triangle.

The Sierpinski Triangle is generated starting from an initial triangle. 3 half-size copies of the triangle are created and placed outside of the original triangle, each of them having a corner “touching” the middle of one side of the triangle.

That procedure is then repeated for each of the 3 new triangles, creating more and more smaller triangles, which progressively fill in an enclosing triangular shape. The figure below uses the same starting point, stopped after 6 “generations”:

The Sierpinski Triangle is an example of a fractal figure, displaying self-similarity: if we were to run the procedure ad infinitum, each part of the Triangle would look like the whole “triangle” itself.

So how could we go about creating this in F#?

I figured this would be a good case for Seq.unfold: given a state (the triangles that have been produced for generation n), and an initial state to start from, provide a function which defines how the next generation of triangles should be produced, defining a “virtual” infinite sequence of triangles – and then use Seq.take to request the number of generations to be plotted.

Here is the entire code I used; you’ll need to add a reference to System.Windows.Forms and System.Drawings for it to run:

open System
open System.Drawing
open System.Windows.Forms

type Point = { X:float32; Y:float32 }
type Triangle = { A:Point; B:Point; C:Point }

let transform (p1, p2, p3) =
let x1 = p1.X + 0.5f * (p2.X - p1.X) + 0.5f * (p3.X - p1.X)
let y1 = p1.Y + 0.5f * (p2.Y - p1.Y) + 0.5f * (p3.Y - p1.Y)
let x2 = p1.X + 1.0f * (p2.X - p1.X) + 0.5f * (p3.X - p1.X)
let y2 = p1.Y + 1.0f * (p2.Y - p1.Y) + 0.5f * (p3.Y - p1.Y)
let x3 = p1.X + 0.5f * (p2.X - p1.X) + 1.0f * (p3.X - p1.X)
let y3 = p1.Y + 0.5f * (p2.Y - p1.Y) + 1.0f * (p3.Y - p1.Y)
{ A = { X = x1; Y = y1 }; B = { X = x2; Y = y2 }; C= { X = x3; Y = y3 }}

let generateFrom triangle = seq {
yield transform (triangle.A, triangle.B, triangle.C)
yield transform (triangle.B, triangle.C, triangle.A)
yield transform (triangle.C, triangle.A, triangle.B)
}

let nextGeneration triangles =
Seq.collect generateFrom triangles

let render (target:Graphics) (brush:Brush) triangle =
let p1 = new PointF(triangle.A.X, triangle.A.Y)
let p2 = new PointF(triangle.B.X, triangle.B.Y)
let p3 = new PointF(triangle.C.X, triangle.C.Y)
let points = List.toArray <| [ p1; p2; p3 ]
target.FillPolygon(brush, points)

let form = new Form(Width=500, Height=500)
let box = new PictureBox(BackColor=Color.White, Dock=DockStyle.Fill)
let image = new Bitmap(500, 500)
let graphics = Graphics.FromImage(image)
let brush = new SolidBrush(Color.FromArgb(0,0,0))
let renderTriangle = render graphics brush

let p1 = { X = 190.0f; Y = 170.0f }
let p2 = { X = 410.0f; Y = 210.0f}
let p3 = { X = 220.0f; Y = 360.0f}
let triangle = { A = p1; B = p2; C = p3 }

let root = seq { yield triangle }
let generations =
Seq.unfold (fun state -> Some(state, (nextGeneration state))) root
|> Seq.take 7
Seq.iter (fun gen -> Seq.iter renderTriangle gen) generations

box.Image <- image

do
Application.Run(form)

A few comments on the code. I first define 2 types, a Point with 2 float32 coordinates (float32 chosen because that’s what System.Drawing.PointF takes), , and a Triangle defined by 3 Points. The transform function is pretty ugly, and can certainly be cleaned up / prettified. It takes a tuple of 3 Points, and returns the corresponding transformed triangle, shrunk by half and located at the middle of the p1, p2 edge. We can now build up on this with the nextGeneration function, which takes in a Sequence of Triangles (generation n), transforms each of them into 3 new Triangles and uses collect to “flatten” the result into a new Sequence, generation n+1.

The rendering code has been mostly lifted with slight modifications from Chapter 4 of Real-world functional programming. The render function retrieves the 3 points of a Triangle and create a filled Polygon, displayed on a Graphics object which we create in a form.

Running that particular example generates the following Sierpinski triangle; you can play with the coordinates of the root triangle, and the number of generations, to build your own!

As an aside, I was a bit disappointed by the quality of the graphics; beyond 7 or 8 generations, the result gets fairly blurry. I’ll probably give a shot at moving this to XAML, and see if it’s any better.

As usual, comments, questions or criticisms are welcome!