Sniff is a "Scratch-like" programming language that's designed to help Scratchers move gently from Scratch to more conventional languages. They can start writing programs, without having to learn a new language because Sniff is based on Scratch. They learn a little more about variables, compiling, syntax errors (!), and they can have fun controlling real hardware while they're doing it.

Thursday, 28 May 2015

Mandelbot sets in Sniff

When I was a postgrad student many years ago, the group was loaned demo unit of a very expensive thermal transfer printer. It wasn't so much that the printer was expensive but the consumables were. For each A3 page it fed through 3 full sheets (CMY) of transfer material, and melted parts of each of them on to the page. The results were A3 photo-quality glossy prints that looked way better than anything you'd find outside a photo-lab even today. Back then colour printers barely existed.

So what did we do with this? Now days you'd download some cool images and print them, but the web and even jpeg wouldn't appear for another year or two. What we did have were some fast (for their day) parallel computers, and so we printed a few mandelbrot sets.

This happened to be just around the time of the big campus open day when thousands of 6th formers visited campus to be wow'ed by the tech on show... and we were asked if we had anything to show? Show? we could go one better. Not only could we show of your super fast parallel computer drawing Mandelbot's, we could sell them prints to take home! We sold dozens for £1 each. To be fair we weren't exploiting the kids - they got a great deal. The prints looked amazing, and those rolls of thermal transfer sheets were crazy expensive. If we'd been paying for the materials we'd have been loosing several pounds on each image, but you know how it is - we were engineering students, tripped out on high powered computers, and we had the demo printer to the end of the week!

Of course the inevitable happened... we ran out of transfer sheets shortly after lunchtime on the open day. Then someone had the best idea that the entire research group ever came up with... The printer had a roll of transfer film, and when it printed a sheet it only used part of it for each page, depending on what was printed. What was left on the film was a negative of what had been printed...

So I wrote a Postscript program to print a black rectangle that filled the whole page, while someone else rewound the whole roll of transfer material back onto the original spool  and put it back in the printer!!! We print the black page and all of the remaining transfer material is dumped onto the page, and we get a negative of the original print - or as we're just printing Mandelbrots, a new print with different colour scheme.

The new batch of prints didn't look quite as good, as the colour schemes weren't what we would have chosen, and the registration was a fraction off, but they still looked pretty amazing, so we sold the second batch for 50p.

Apparently the sales rep was a little shocked when he collected the demo printer, and discovered we'd used all the transfer film. He was even more surprised to find we'd used all the transfer film - every single last pixel of it!

Maths

Mandelbrots are based on a very simple equation
Z'=Z*Z+C

We have a value Z, we square it and add something to it, and that gives us a new value for Z. We start with Z=0 and use the coordinates of the pixel as the value for C. We repeat this until Z "gets big", and count how long it takes to get big - if it never gets big, then its "in the set", and we colour bit black. Otherwise we colour it based on how long it took to get big.

Simple, except that C isn't a "value" its a position on the screen. Rather than calling the coordinates x and y we call them "real" and "imaginary", so

Z=Zr+Zi
C=Cr+Ci

So now

Z'=(Zr+Zi)*(Zr+Zi)+Cr+Ci

or multiplying all that out:

Z'=Zr*Zr+2*Zr*Zi+Zi*Zi+Cr+Ci

but then need to split Z' into Z'r and Z'i. There are rules for how we split things that have been multiplied:
r*r=>r
r*i=>i
i*i=>-r

So 
Z'r = Zr*Zr-Zi*Zi+Cr
Z'i = 2*Zr*Zi+Ci



Code

Here's the core code:

when start
.broadcast setupColors and wait
.repeat 640 using displayX
..repeat 480 using displayY
...set Cr to (displayX-320)/160
...set Ci to (displayY-240)/160
...set Zr to 0
...set Zi to 0
...set count to 0
...repeat until count = length of colours or Zr*Zr+Zi*Zi>4
....set ZZr to Zr*Zr-Zi*Zi+Cr
....set ZZi to 2*Zr*Zi+Ci
....set Zr to ZZr
....set Zi to ZZi
....change count by 1
...set displayColor to item count of colours
...tell display to "setPixel"

And here it is in Sniff.

We loop over every pixel in a window (640 by 480), and based on that pixel we calculate a value for C (Cr from -2 to + 2, Ci slightly less, but with the same scale so it stays square). Then we initialise Z to 0, and iterate round calculating Z'. We stop when Z*Z>4 as once we go outside this circle Z gets very big very quick and we can stop counting.

We keep a count of how many times we've been around, and use that to pick a colour. The coloured points are outside the set, but they show how close we're getting. To make things nice and simple I created a list of colours, so you can fill in any colours you like - my colour scheme isn't very inventive When we run out of colours to use, we give up, and colour it black.

And here's the full code, including the setup of all the devices, and the colour list:

make nativeFile device
make display window device
make displayX number
make displayY number
make displayColor number

when start
.forever
..tell display to "getEvent"
..wait 0.1 secs

make colours list of numbers
when setupColors
.add 777 to colours
.add 700 to colours
.add 440 to colours
.add 070 to colours
.add 044 to colours
.add 007 to colours
.add 404 to colours
.add 700 to colours
.add 440 to colours
.add 070 to colours
.add 044 to colours
.add 007 to colours
.add 404 to colours
.add 700 to colours
.add 440 to colours
.add 070 to colours
.add 044 to colours
.add 007 to colours
.add 404 to colours
.add 700 to colours
.add 440 to colours
.add 070 to colours
.add 044 to colours
.add 007 to colours
.add 404 to colours
.add 700 to colours
.add 440 to colours
.add 070 to colours
.add 0 to colours

make Cr number
make Ci number
make Zr number
make Zi number
make ZZr number
make ZZi number
make count number
when start
.broadcast setupColors and wait
.repeat 640 using displayX
..repeat 480 using displayY
...set Cr to (displayX-320)/160
...set Ci to (displayY-240)/160
...set Zr to 0
...set Zi to 0
...set count to 0
...repeat until count = length of colours or Zr*Zr+Zi*Zi>4
....set ZZr to Zr*Zr-Zi*Zi+Cr
....set ZZi to 2*Zr*Zi+Ci
....set Zr to ZZr
....set Zi to ZZi
....change count by 1
...set displayColor to item count of colours
...tell display to "setPixel"

Try changing the colour scheme. Also try changing the range of values for Cr/i to see different parts of the set. e.g.: 

...set Cr to ((displayX-320)/320)*0.2-0.65
...set Ci to ((displayY-240)/320)*0.2+0.4


And of course to get it running on Arduino, just change the "window" device to be a "tft", device.

By co-incidence the 320x240 screen is pretty close to the resolution of the BBC micro Mode 1 screen that I first ran this sort of code on. The difference is that the BBC Micro took hours to generate this kind of image, while even an Arduino can do it in a minute or two. On a PC at 640x480 it takes only a second! 

No comments:

Post a comment