I once wrote a
rant about how, in 1982, it had seemed very easy to draw graphics on the screens of computers, and that nowadays it seems hard, and wondering how a child nowadays is supposed to start writing the graphics programs that were the best bit of programming when I was young.
This rant got on to the front page of reddit for a full day, and was the top hit on the programming reddit for several, and got voted on and commented on so much that I think that a few hundred thousand people must have read it.
About one third of people agreed with me, about one third disagreed and showed me ways to do it, and lots of kids said that yes, computers were incomprehensible, but they quite enjoyed programming their pocket calculators, which these days have screens and BASIC and are essentially portable ZX81s.
In the end, I ended up being convinced by the optimists, and wrote
a program for twelve year old programmers which uses the turtle module that is built into python, and relaxed, knowing that the kids would be OK, and figuring that I could probably muddle through.
But it did still seem to be true that there was no effort at all to plotting on the first computers I had, and then later with Turbo Pascal it was a question of copying a boilerplate preamble to set up the graphics card and then off you go in the same way, and then windows arrived and all hell broke loose and it was ages before I learned how to make pictures again.
I've written some windows graphical programs, but it was always a chore. Java made it easier, but it was still a bit of a pain. And notwithstanding python's turtle module, it's always a pain in every language I use, and it never seems easy or fun like it used to do.
The other day, I wanted to plot something. And I started wondering gloomily how to do it from clojure. And the easiest way seemed to be to import incanter and use its graphics libraries, which are intended for statistics and make lovely pictures of functions, but don't quite seem to be the sort of thing that I remember from when I was a boy.
And I thought: Scratch that itch.
So I downloaded a ZX Spectrum emulator
$ sudo apt-get install spectemu-x11
Fully expecting to be badly disappointed.
But no. First of all, the emulator's great. It's so like the real thing was that I could feel old habits and reflexes coming back. Well done whoever wrote it. And the
ZX BASIC manual is just as well written as I remembered. I can quite see how this marvellous little device got me hooked.
What I wasn't expecting was how impressive the spectrum itself was. I've designed and programmed embedded devices with about the same amount of power, and these guys took what was basically a glorified washing machine controller, a TV output, and a rubber keyboard they invented themselves and made a wonderful little computer out of it that cost half as much as it would have cost to buy just the keyboard for the first IBM PC, which launched around the same time and actually used a very similar glorified washing machine controller, but cost about what my father earned in a month when I was 12.
You would expect it to have been utter rubbish, but it's really not. It takes about 20 minutes to get used to its strange keyboard and line based editor (vi was invented in 1976, and at the time was a vast thing that ran on computers that cost hundreds of thousands of pounds), and after that you're just talking directly to the little processor in BASIC.
With hindsight, those guys must have been awesome hackers. And awesome designers. And awesome industrial-process men, and awesome engineers, and awesome marketers. My God they must have been good.
And it may be somewhat limited, but a child could learn to program on one of these even these days, and it would be fun.
And I spent a happy few hours plotting things. Without having to think about how a java.awt.Graphics2D.DoubleBuffered.Image.ofDespair plugs into an OverComplicatedInterfaceAdapterFactoryProxy so it can be ........
And once my itch was scratched, I thought that it would be quite easy to make a little library for clojure that worked in the same way as the Spectrum's plotting routines. So I looked up some old java programs that I had written back in the days when Java seemed fresh and new and sane, and this is what I came up with:
(use 'simple-plotter)
(create-window "ZX Graphics" 256 176)
(doseq [x (range 256)]
(plot x (+ 88 (* 80 (Math/sin (* Math/PI (/ x 128)))))))
(ink gray)
(plot 0 88) (draw 255 0)
(plot 127 0) (draw 0 175)
And then I started to think that it was a bit much that ZX BASIC, which really is not a terribly expressive language, seemed to have things to teach clojure about plotting, and I thought of Barnsley's lovely fractal fern, which would be a big and difficult program to write on the Spectrum.
(use 'simple-plotter)
(defn transform [[xx xy yx yy dx dy]]
(fn [[x y]] [(+ (* xx x) (* xy y) dx)
(+ (* yx x) (* yy y) dy)]))
(def barnsleys-fern '((2 [ 0 0 0 0.16 0 0 ])
(6 [ 0.2 -0.26 0.23 0.22 0 0 ])
(7 [ -0.15 0.28 0.26 0.24 0 0.44 ])
(85 [ 0.85 0.04 -0.004 0.85 0 1.6 ])))
(defn choose [lst] (let [n (count lst)] (nth lst (rand n))))
(defn iteration [transforms]
(let [transform-list (mapcat (fn [[n t]] (repeat n (transform t))) transforms)]
(fn [x] ((choose transform-list) x))))
(def barnsley-points (iterate (iteration barnsleys-fern) [0 1]))
(create-window "Barnsley's Fern" 350 450)
(ink green)
(scaled-scatter-plot (take 10000 barnsley-points) 50 300 50 400 100)
And that appears to have handed ZXBASIC its ass on a plate, if you will pardon the expression.
And doing graphics programming is fun again.
Although performance-wise, my little library appears to have much in common with the ZX Spectrum.
No matter. I know that I can make it run at least 300 times faster without too much trouble. I am fairly confident that I will be writing more programs using it, and I am sure that one day I will find something that needs it to be faster, and take the trouble to sit down with performance tuning tools and sort it out.
For the moment, I am still working on the interface, which is the important bit. After that, algorithms and correctness. Once it's all nice and stable, I'll probably translate it into Java, or Clojure 1.3, which I am told may be able to achieve Java-like performance with minimal tuning and type hints.
Here's my little library. Sorry about the code. It isn't well written, it's very slow, and I haven't taken the trouble to comment or explain it, because I'll have completely rewritten it in a couple of days. I post it in case anyone else misses their Spectrum.
(ns simple-plotter
(:import (javax.swing JFrame JPanel )
(java.awt Color Graphics Graphics2D Image))
(:use (clojure.contrib def)))
(defmacro- defcolours [& colours]
(list* 'do (map #(list 'def (symbol (. (str %) toLowerCase)) (symbol (str "Color/" (str %)))) colours)))
(defcolours black blue cyan darkGray gray green lightGray magenta orange pink red white yellow)
(defn- draw-lines [lines #^Graphics2D g2d xs ys]
(doseq [[x1 y1 x2 y2 color] @lines]
(. g2d setColor color)
(. g2d drawLine (* xs x1) (* ys y1) (* xs x2) (* ys y2))))
(defn- render-lines-to-graphics [lines paper-color height width
#^Graphics2D g w h ]
(doto g
(.setColor @paper-color)
(.fillRect 0 0 w h))
(draw-lines lines g (/ w @width) (/ h @height)))
(defn- primitive-repaint [plotter]
(. (plotter :panel) repaint))
(defn create-plotter [title width height ink paper]
(let [lines (atom [])
height (atom height)
width (atom width)
paper-color (atom paper)
panel (proxy [JPanel] []
(paintComponent [g]
(proxy-super paintComponent g)
(render-lines-to-graphics
lines paper-color height width
#^Graphics2D g
(. this getWidth)
(. this getHeight))))
frame (JFrame. title)]
(doto frame
(.add panel)
(.setSize (+ @width 2) (+ @height 32))
(.setVisible true))
{:height height
:width width
:ink-color (atom ink)
:paper-color paper-color
:current-position (atom [0,0])
:lines lines
:panel panel}))
(defn- primitive-line [plotter x1 y1 x2 y2]
(let [ink @(:ink-color plotter)]
(swap! (:lines plotter) conj [x1 y1 x2 y2 ink]))
(primitive-repaint plotter))
(defn- set-paper-color [plotter color]
(swap! (plotter :paper-color) (constantly color))
(primitive-repaint plotter))
(defn- set-ink-color [plotter color]
(swap! (plotter :ink-color) (constantly color)))
(defn- set-current-position [plotter [x y]]
(swap! (plotter :current-position) (constantly [x y])))
(defn- remove-lines [plotter] (swap! (plotter :lines) (constantly [])))
(defn- make-scalars [points xleft xright ytop ybottom]
(let [xmax (reduce max (map first points))
xmin (reduce min (map first points))
ymax (reduce max (map second points))
ymin (reduce min (map second points))]
[(fn[x] (+ xleft (* (/ (- x xmin) (- xmax xmin)) (- xright xleft))))
(fn[y] (+ ybottom (* (/ (- y ymin) (- ymax ymin)) (- ytop ybottom))))]))
(defvar- current-plotter (atom nil))
(defn create-window
([] (create-window "Simple Plotter"))
([title] (create-window title 1024 768))
([title width height] (create-window title width height white black ))
([title width height ink paper]
(let [plotter (create-plotter title width height ink paper)]
(swap! current-plotter (constantly plotter))
plotter)))
(defmacro ddefn [fnname args & body]
`(defn ~fnname
(~args (~fnname @~'current-plotter ~@args))
([~'plotter ~@args] ~@body)))
(ddefn plot [x1 y1]
(primitive-line plotter x1 y1 x1 y1)
(set-current-position plotter [x1 y1]))
(ddefn cls []
(remove-lines plotter)
(primitive-repaint plotter))
(ddefn plot [x1 y1]
(primitive-line plotter x1 y1 x1 y1)
(set-current-position plotter [x1 y1]))
(ddefn draw [dx dy]
(let [[x1 y1] @(plotter :current-position)
[x2 y2] [(+ x1 dx) (+ y1 dy)]]
(primitive-line plotter x1 y1 x2 y2)
(set-current-position plotter [x2 y2])))
(ddefn line [x1 y1 x2 y2]
(plot plotter x1 y1)
(draw plotter (- x2 x1) (- y2 y1)))
(ddefn ink [color] (set-ink-color plotter color))
(ddefn paper [color] (set-paper-color plotter color))
(ddefn scaled-scatter-plot [points xleft xright ytop ybottom scalepoints]
(let [[xsc ysc] (make-scalars (take scalepoints points) xleft xright ytop ybottom)]
(doseq [[x y] points]
(plot (* (xsc x))
(* (ysc y))))))
(defn window [plotter]
(swap! current-plotter (fn[x] plotter)))
(ddefn get-height [] @(plotter :height))
(ddefn get-width [] @(plotter :width))
(defn sine-example[]
(create-window "sine")
(cls)
(doseq [x (range 1024)]
(plot x (+ 384 (* 376 (Math/sin (* Math/PI (/ x 512)))))))
(ink yellow)
(plot 0 384) (draw 1024 0)
(line 512 0 512 1024))