About D. Scott Williamson

Compulsively Creative

Atari 8 Bit Mandelbrot Set Zoom Videos

In a previous blog post I explained all about the color cycling Mandelbrot Set explorer I wrote in 10 lines of Atari Basic for the BASIC 10 Liner contest (wish me luck!). While working on this project I thought it would be really cool to create a Mandelbrot Set Zoom video rendered on the Atari, which led me on an interesting journey…

I wanted to make something like this deep Mandelbrot Set zoom video

Or this video I found later that animates the color palette while zooming.

The Atari BASIC Mandelbrot Set renderer is not able to execute millions of calculations for every pixel, as a matter of fact it is configured to execute 81, at that depth the numerical precision of Atari BASIC’s floating point numbers appears to start breaking down, and with it’s 1.79MHz 6502 CPU, more cycles would start to take a very very long time.

The program is able to view 12 interesting preset locations on the complex plane of the Mandelbrot Set and is able to zoom in and out by doubling and halving the scale of the screen. I realized that if you captured a screen at each size I could scale the bitmap and make an image that had very high resolution (small pixels) in the center, and then could zoom in on it. I also thought if I captured video of the color cycling I could scale and synchronize the videos to zoom in while color cycling, and by offsetting the color cycling by a fixed amount with each zoom level I could create synchronized color coded animated frames that better showed the zoom levels.

I reached out to fantastic Atari chiptunes artist Adam Sporka and he generously offered to share his music for use in these videos which is done on a real Atari 800 . I think it is totally awesome that all graphics and audio were created on Atari 8 bit computers.

I won’t keep you in suspense forever

Here are 6 of the 24 resulting Atari BASIC Mandelbrot Set zoom videos, in the ones with frames each colored frame is half or twice as large as adjacent frames:

Mandelbrot Zoom Videos

Videos with colored frames

Here are complete YouTube playlists of all 24 of the Mandelbrot Set zoom videos: 12 locations rendered without and 12 with colored frames. The first 3 videos in each playlists are the ones above, so if you’ve already seen them you may want to skip ahead to see the remaining 9 videos in each playlist.

Here’s how I did it…

In the Atari800Win-PLus emulator I ran the program, visited each location, zoomed all the way in and all the way out, each time taking ~30 minutes to render and then recording a ~20 second video. The 297 videos took most of a weekend to render and capture. I ran the emulator in 12 Windows XP Virtual machines in VirtualBox on my (KDE Kubuntu) laptop so was able to keep the process moving. This animated .gif was captured from the desktop using Peek while I was doing these captures.

When capturing video, the emulator has a limited set of video codec’s to choose from: Cinepack Codec by Radius, Microsoft Video, Microsoft RLE, and Intel IYUV. When testing, Cinepack did not seem to be compatible any more, and I decided to use Intel IYUV format because it was compact, and looked good. I did run into a horrifying problem: After all the video was captured I looked at in in VLC on the laptop and bizarrely the video was mirrored. This appears to be an issue with the Linux version of the playback codec, and to my relief did not pose a problem in Windows.
(* WHEW! *)

I used folder sharing in VirtualBox to capture the videos from all the emulators running in the virtual machines into a single shared folder, and then I copied them all to a folder shared on my Windows machine (via Samba).

Each of the 12 locations had several videos with numbered filenames like “Thorny Lightning 0.avi”, “Thorny Lightning in 2.avi” or “Thorny Lightning out 3.avi” where the “in” and “out” indicated the number of times it was zoomed from the default zoom level named “0“.


I thought I could use video editing software to simply composite the Mandelbrot Set zoom but I was wrong, very wrong. I though I would manually set the in and out point in each of the 297 video clips in video editing software, align them on the timeline, set some zoom interpolation, and voila! … but when I tried a test sample I discovered two massive problems:

  1. The video zoom effect is not linear, it is exponential, it doubles in size at regular time intervals. This is something that is much harder to do in video editing software, and is VERY hard to synchronize across several aligned composited color cycling videos.
  2. The color cycling was not synchronized between videos. Even though I could perfectly match the first and last frames of a color cycle sequence in each video and scaled the videos on the timeline to the same length, the middles of the color cycling sequence were often out of sync leading to flickering that ruined the quality of the video (and yet gave me the idea for the colored frame version of the videos).

Handling zoom

The only way I thought to handle the zoom issue was to programmatically composite the frames. This would require exporting the frames from the .avi files, identifying the synchronized frames from each of the source avi’s, and compositing them together so all of the images were scaled and registered to one another perfectly while zooming exponentially.

For processing I extracted all the frames of each .avi video into numbered bitmaps in a _frames folder using FFmpeg. To do so I used an hybrid manual automated process, which is a fancy way of saying I kept editing a batch file until I got everything I needed. The batch file basically looked like this:

for /r %%i in ("..\Thorny Lightning*.avi") do (
ffmpeg -i "%%~pi%%~ni.avi" -filter:v "crop=320:192:10:24" "_frames\%%~ni %%04d.bmp"

This would process all the .avi files from a particular location, in this case “Thorny Lightning”, using a wildcard in a for loop. The script calls FFmpeg once on each .avi file that matches the wildcard, inputs the .avi, crops the black overscan border from the images and saves them as numbered bitmaps in the “_frames” folder. After processing all 297 videos I have 413,153 numbered .bmp files (70GB).

To composite the frames in an exponential zoom I wrote a Python script that uses ImageMagick to scale and composite the source .bmp frames into zooming video clip frames. The scaling ended up being simpler than expected. Since the image grows geometrically with time, the scale based on time t is 2t and since each video is half the size of the next largest I could divide the dimensions in half for successive frames, all centered at the center of the image. For each output image the program generates command lines similar to this one to execute ImageMagick:

magick convert -size 320x192 -gravity center ( "_frames\Thorny Lightning out 15 0133.bmp" -sample 329x198 ) ( "_frames\Thorny Lightning out 14 0144.bmp" -sample 165x100 ) -composite ( "_frames\Thorny Lightning out 13 0120.bmp" -sample 83x51 ) -composite ( "_frames\Thorny Lightning out 12 0140.bmp" -sample 42x26 ) -composite ( "_frames\Thorny Lightning out 11 0109.bmp" -sample 22x14 ) -composite ( "_frames\Thorny Lightning out 10 0116.bmp" -sample 12x8 ) -composite ( "_frames\Thorny Lightning out 9 0130.bmp" -sample 7x5 ) -composite ( "_frames\Thorny Lightning out 8 0128.bmp" -sample 4x3 ) -composite ( "_frames\Thorny Lightning out 7 0121.bmp" -sample 3x2 ) -composite ( "_frames\Thorny Lightning out 6 0120.bmp" -sample 2x2 ) -composite ( "_frames\Thorny Lightning out 5 0129.bmp" -sample 2x2 ) -composite -crop 320x192+0+0 +repage "Thorny lightning\frame_ioi_00000005.bmp"

This line makes a 320×192 bitmap composited of 11 scaled source bitmaps. Finding the right settings to get ImageMagick to composite the videos the way I wanted with cropping and point sampling (instead of anti-aliasing) was a challenge. It is a very powerful tool that can process images in a multitude of ways, often offering many ways to accomplish the same or similar results (ImageMagick reference).

The source color cycling videos were manually captured so contain more than a single clean synchronized color cycle loop (extra frames at the beginning and at the end of the video), so I manually looked at the bitmaps to find the index of the first and last frames of the color cycle and included those into the program to create a test video. The resulting video finally zoomed perfectly but was still ruined by flickering due to the unsynchronized color cycling.

Digging a little deeper…

Atari BASIC is not frame synced, meaning it does things continuously and without synchronization with the TV display signal. The Atari computer emulator on the other hand renders video at 60Hz, one frame for every NTSC field emulated.

I started performing analysis of the bitmap sequences in Python using Pillow (fork of the Python Image Library or PIL) for image processing. Initially I tried to detect the number of colors in an image to decide when the color cycling was changing but that was vexed by the scan lines effect I had enabled in the emulator which caused anti-aliasing of some of the lines and a lot more than the 9 Atari graphics mode 10 colors I was expecting (I still think it looks cool). While performing additional experimentation I noticed that successive video frames were changing while color cycling was copying one register to the next and calculating a new color, but there were many duplicate frames while the rest of the Atari BASIC program executed its processing loop. Additionally I was aware that at one point in the color cycling all of the onscreen colors would be gray scale (actually close but not perfectly RGB gray).

The winning strategy

I scan each bitmap sequence from a video from the beginning to find the first frame that is entirely gray scale, then I scan from the back for the first frame of the same gray sequence at the end, this defines the range of frames for one full color cycle. Within that range of frames I compare each frame with the next until I find a set of duplicate frames, and store the first duplicate frame’s index. There are exactly 128 frames in a color cycling sequence: 8 brightness cycles of 16 hues. Adding a multiple of 8 will synchronize brightness but will offset hue, this is used for the colored frames effect. When the process is done it should have discovered the exact 128 frames representing one color cycle for that bitmap sequence (from the .avi) at that zoom level.

More complexity

This almost worked perfectly, except Atari BASIC is not perfect and I am not perfect. Atari BASIC would occasionally let an extra duplicate frame occur, and I occasionally recorded two or three color cycles that would result in 256 or 384 (or more) frames instead of the expected 128 frames: in either case it messed things up.

More fixes

The unwanted duplicate frames were pretty easy to find; usually duplicate frames were 9 or 10 frames apart, but when an extra is inserted, they will be only 4 to 6 frames apart. When the program detects anything other than 128 frames in a cycle it dumps all the frame offsets and the number of frames between offsets. I manually tracked down the extra frames and defaced them in Gimp making them no longer duplicates. I re-recorded the videos I screwed up (and deleted the bitmaps I made, and made new bitmaps with FFmpeg), and was back in business.

What now?

By now Adam had agreed to share his music and I had to figure out which songs I thought matched with each video. To do that I created some more batch files this time using FFmpeg to create .mp4 video files from the exported .bmp frames and the .mp3 music Adam provided. I used command lines like this to generate test videos with music:

ffmpeg -y -r 60 -i "Thorny Lightning\frame_ioi_%%08d.bmp" -i "music\Stack 'Em up Adam Sporka.mpeg" -c copy -map 0:v:0 -map 1:a:0 -r 60 -c:v libx264 -pix_fmt yuv420p -preset slow -crf 19 -vf scale=iw*4:ih*4 -sws_flags bitexact "_out\Thorny Lightning ioi x4.mp4"

This reads the bitmap sequence at 60fps and the music Adam provided, mapping the bitmap sequence channel 0 to video, and the music channel 1 to audio at an output framerate of 60fps using the h264 codec with very low loss, at 4 times the resolution (if I uploaded the video to YouTube rumor has it increasing the resolution will increase the quality) with the bitexact flag telling it to resample the image rather than use a smoothing enlarging filter.

Here is a similar simpler command line to generate only a video with no sound.

ffmpeg -y -r 60 -i "Thorny Lightning\frame_ioi_%%08d.bmp" -r 60 -c:v libx264 -pix_fmt yuv420p -preset slow -crf 19 -vf scale=iw*4:ih*4 -sws_flags bitexact "_out\Thorny Lightning ioi q x4.mp4"

These were excellent tests to adjust the color cycling and zoom rates and see which of Adams songs matched 12 Mandelbrot Set locations, but the videos had no titles, credits, or transitions. They were previews but they lacked finished polish.

Are we there yet?

I used HitFilm Express to edit the 12 video projects and render 24 videos. I imported the bitmap sequences as clips (didn’t use the preview compressed .mp4’s), created intro and outro titles, composited the video with the awesome music provided by Adam Sporka, and synced up all the fades. I exported two versions of each video, one with colored frames, one without. Then uploaded them to YouTube, made the descriptions, and end cards, and etc. You know the rest.

In conclusion

In the end I love the way the videos (and this blog) turned out, it’s amazing to think that everything you see and hear in these videos was created on Atari computers. The Atari BASIC script that rendered every frame was only 10 lines long but it took VirtualBox running 12 Atari800Win-PLus emulators, half a dozen batch files, FFmpeg, ImageMagick, Gimp, 560 lines of Python using Pillow, and finally HitFilm Express to generate the final videos.

Stay creative, and support your creative communities!

An interactive Mandelbrot set explorer in 10 lines of Atari BASIC

Table of Contents


    This project is an interactive Mandelbrot set explorer able to zoom in and out of twelve classic fractal locations featuring a multi-resolution renderer, title screens, sound, and nine selectable color cycling patterns ranging from soothing to psychedelic, all in only 10 lines of Atari BASIC.

    I was inspired by the Hackaday article Basic In 10 Lines Or Less which led me to the annual Basic 10 Liner Contest page (English rules start halfway down the page). There you can see current and previous entries spanning many years. The basic idea is to write the best game, application, or demo in 10 lines or less of BASIC on your favorite platform. There are three game categories distinguished by maximum line length and one “SCHAU” or “Look” category for “a demo, a tool or an application program” into which this project will be entered.

    What is the Mandelbrot set?

    You’re probably familiar with the lightning enshrouded circle and cardioid fractal images that resemble colorful spirals and paisley when examined more closely.

    From Wikipedia: The Mandelbrot set (/ˈmændəlbrɒt/) is the set of complex numbers c for which the function {\displaystyle f_{c}(z)=z^{2}+c} does not diverge when iterated from z = 0 , i.e., for which the sequence {\displaystyle f_{c}(f_{c}(0))}, etc., remains bounded in absolute value. Its definition is credited to Adrien Douady who named it in tribute to the mathematician Benoit Mandelbrot, a pioneer of fractal geometry.

    The colors in these images represent the number of times the Mandelbrot function is iterated before it diverges. Central areas (typically black) are areas that either do not diverge (infinite iterations) or are beyond the number of iterations the program is capable of performing. This program is configured to perform 81 iterations. There are YouTube “Mandelbrot Set Zoom ” videos that reach magnifications requiring hundreds of millions of iterations (like this one).

    The Mandelbrot set has an unknown but bound area probably just under 1.5065 yet the perimeter is infinitely long and complex. The more you zoom in on the edge, the more twists and convolutions are revealed. There are several “mini” Mandelbrot sets all around the main form and they are all connected.

    This Program’s Mandelbrot Set Locations

    There are many well known locations around the Mandelbrot set that have unique features and appearances that give them their colloquial names including: Seahorse Valley, Elephant Valley, Sceptre Valley, and The Needle.
    Below are the locations featured in this program.


    Download and install Atari emulator

    This section describes how to download, install, configure and use the Atari800Win-PLus emulator. If you do not need to install and configure this emulator you may skip this section.

    I’ve always used Atari800Win-PLus on Windows (download)

    Atari DOS Mandelbrot disk image containing MANDELBRO.BAS and MANDELBRO.LST:
    Mandelbrot Set Explorer – 10 lines Atari BASIC.atr

    Emulator configuration

    Set the machine type to OS-B

    Make sure your ROM images are loaded.
    If they are missing you can download them from Github here:

    Set your video system to NTSC
    You can set it to PAL but he color cycling and possibly rendering will be 1/6 slower. Otherwise it should make no difference on a PC Emulator.

    “Insert” the Mandelbrot Set Explorer – 10 lines Atari BASIC.atr disk image in D1:
    Atari menu, Disk Drives option or Alt-D
    Alternately, you can drag and drop the file onto the running emulator. (you may need to re-enable the BASIC cartridge after drag and drop)

    Configure input, you will need to emulate joystick 0 and its button.
    I use the default:
    Arrows +RCTRL as fire
    Disable Autofire
    In Advanced…
    Check Allow using keyset keys also as regular keys

    In Misc menu Preferences uncheck Stop when inactive
    This will allow you to let the emulator render in the background while the window does not have focus.

    In the View menu select Graphics options… (Alt+G)
    The smaller window will render faster, but larger sizes are acceptable.

    In View menu set Artifacting… to GTIA

    In View menu set Stretching to Scan lines
    (my personal preference)

    Function keys

    • F7 Toggle Unlimited speed
      (to render more quickly, ~30x-50x, I highly recommend using this when rendering)
    • F10 Capture image
    • F11 Enable fast disk IO
      (Turn this on and forget it)

    Loading, listing, and running the program

    Configure Atari as indicated above, make sure BASIC cartridge is enabled, and Mandelbrot Set Explorer – 10 lines Atari BASIC.atr is inserted in D1: then boot or reset the Atari.

    To load the Atari Basic program, at the READY prompt enter:

    Alternately you may enter the listing by entering: (they are the same)

    If you would like to see the program (optional) listing enter:
    Note: All of the abbreviations will be automatically expanded, and if you try to edit the lines on the Atari they will be too long. See How I developed this program below for information about how to edit this program.

    To run the program enter:

    After a few seconds of initialization you should see the title screen for the first area – Mandelbrot Set at (0,0).

    Remember: F7 toggles normal and full speed (muted) which is very useful to speed the rendering.

    Program Usage


    Joystick (typically arrow keys in emulator)

    • Up – Zoom in (enlarge image to double its size)
    • Down – Zoom out (shrink image to half its size)
    • Right – Move to the next of 12 locations and reset zoom level to default for location
    • Left – Move to previous of 12 locations and reset zoom level to default for location

    Joystick Button (typically Right CTRL in emulator)

    • Title screen: Exits title screen to renderer
    • While rendering: Cycles through color cycling modes

    Due to potentially lengthy computations, input may be slightly delayed. Sounds confirm will confirm inputs to the user (Note: when running AtariWin800-PLus at full speed using F7, the emulator does not emit sound)

    Title Screen

    Mandelbrot Set is the first of the 12 preset locations

    A title screen is displayed whenever moving to a new preset location or changing zoom level and it displays the following information:

    • Nickname of location (“MANDELBROT SET”)
    • Center coordinates X,Y
    • Zoom Z (magnification level, screen x extends from -1 to 1 at zoom 1)

    Numbers may be expressed in scientific notation.
    For example 2.5e-5 means 2.5 * 10-5 or 2.5 * 0.00001 which equals 0.000025.

    These coordinates are compatible with all other Mandelbrot set exploration tools.

    Press the joystick button to exit the title screen and begin rendering the Mandelbrot Set!

    Mandelbrot set
    multi-resolution rendering

    Multi-resolution rendering in progress

    It takes time to perform the many calculations needed to populate the 15,360 pixel display. Rather than slowly raster fill the display (left to right, top to bottom) this program uses a progressive multi-resolution rendering technique. It starts by filling the 80 column display with three evenly spaced rows stretched vertically 64x to fill the entire 192 line display. Then successive passes twice as many rows (after the second pass), half as tall as the previous pass until the last pass fills in every other row of the display.

    Each pixel is calculated exactly once, each 80 pixel row is still calculated from left to right, each row takes roughly the same amount of time to render regardless of its height, and each pass doubles the resolution as the previous pass (as a consequence it also takes about twice as long).

    This approach takes the same amount of time to fully draw a screen but a low resolution image is rendered quickly providing a preview that is continuously refined until complete. This quick preview is helpful when zooming and switching locations.

    Multi-resolution rendering passes:

    While rendering, a single pixel tall cursor is displayed to let the user more easily locate the current rendering location.

    Tip: Rendering a single image at normal Atari speeds can take 16 hours or more depending on the complexity, press F7 to toggle “Run Atari as fast as possible” and they can be rendered in 15 ~ 30 minutes.

    Color Cycling

    Color cycling is technically moving color values from color register to color register, instantly changing the colors on the screen without having to change pixels. Rather than cycling 8 values through the 8 color registers to repeat a short pattern, this program cycles 7 colors then adds a constant to the value in the 8th register to cycle through interesting color sequences from 8 to 128 colors in length.
    Color cycling is performed continuously when the image is fully rendered and with each pixel calculation while rendering.
    Pressing the joystick button will cycle through the 9 color cycling modes above (with a beep).

    Detailed description of each of the 9 modes (pictured in left to right top to bottom order above):

    1. Decreasing intensities then decreasing hues (brightest color first)
    2. Increasing intensities then increasing hues (darkest color first, reverse of 1)
    3. Eight increasing hues through eight color registers at constant intensity for continuous hue cycling.
    4. Increasing hues while descending in intensities (brightest color first)
    5. Increasing hues while increasing intensities (darkest color first)
    6. Increasing hues of constant intensity (since there are only 8 color registers and 16 hues, only half the hues will be visible in a sliding window fashion)
    7. Decreasing hues while descending in intensities (brightest color first)
    8. Decreasing hues while increasing intensities (darkest color first)
    9. Decreasing hues of constant intensity (since there are only 8 color registers and 16 hues, only half the hues will be visible in a sliding window fashion)

    Interesting notes about the color cycling modes:

    • Modes in the left column all cycle with the brightest color first
    • Modes in the second column cycle darkest color first
    • Modes in the right column only change hue and not brightness (excellent for seeing the boundary of the Mandelbrot Set)
    • The first two columns in the first row cycle through intensity then hue
    • The first two columns of the last two rows cycle through intensity and hue in each step

    Detailed Program Description

    10 lines less than 256 characters long

    Maximum line length for the contest is 256 characters. The image below shows the longest line (3) is 216 characters long.

    Note: Spaces needed to be added after comas in DATA statements below to permit WordPress to wrap lines.

    Line by line functional description

    1LS=10:MC=81:DI.N$(192),Q(159):F.I=0TOLS-1:F.J=0TO7:Q(78+I*8+J)=8-J:N.J:N.I:F.I=0TO77:REA.X:Q(I)=X:N.I:Z=Q(24):N$="mandelbrot set ":L=0:T=L:Q(158)=L:DC=T

    Line 1 is a one time program initialization.

    • LS=10 LS is the number of color cycles in the solution.
    • MC=81 MC is the maximum Mandelbrot Calculations depth which is 8*LS+1. The 8 is the number of colors in the screen palette which will be repeated LS times, and the extra one is for the black interior of the unreachable Mandelbrot calculations (see Q(158) below).
    • DI.N$(192),Q(159) Dimension N$, and data array Q. N$ holds the 12 name streams for each location, each 16 characters long (12 * 16 = 192). Q is an array that holds program constants and precalculated data. See Managing program lookup tables and constants below.
    • F.I=0TOLS-1:F.J=0TO7:Q(78+I*8+J)=8-J:N.J:N.I
      Nested I and J loops to precalculate color cycling data in array Q to be used in rendering.
      • F.I=0TOLS-1 The I FOR loop counts through all LS color cycles
      • F.J=0TO7 The J FOR loop counts through 8 colors.
      • Q(78+I*8+J)=8-J stored the color value to use for index 0 to 80 (I * 8 + J) which repeatedly counts down from 9 to 1 (8 – J)
    • F.I=0TO77:REA.X:Q(I)=X:N.I
      FOR I loop reads 78 program constants from DATA statements and places them in the master data array Q for random access later. See Managing program lookup tables and constants below.
    • Z=Q(24) Zoom factor Z is loaded from the first location of 12 Zoom elements in the constants array.
    • N$="mandelbrot set " N$ is the area name array, it is temporarily initialized with the first level’s name.
    • L=0:T=L:Q(158)=L:DC=T
      • L=0 Location index L is initialized to zero.
      • T=L The top T of multi-resolution pixels is set to zero by copying L (smaller tokenized line length).
      • Q(158)=L Q(158) is the last color in the color cycle table and is set to border color zero which is black, again by setting equal to L. This color will be used for all of the pixels with unreachable complexity meaning the function does not diverge to infinity in the 81 iterations (defined by Mandelbrot Calculations variable MC) – these are the black areas of the Mandelbrot images.
      • DC=T Delta Color index DC is set to zero by copying T. DC is used as an index to lookup the actual delta value that will be added to the color value to achieve the 9 color cycling modes.

    2CX=Q(L):CY=Q(L+12):D=64:D2=D*2:H=D-1:GR.18:I=L*16+1:?#6;N$(I,I+15),,,"x";CX,"y";CY,"z";Z:?#6,,,,"press JOY BUTTON":F.I=0TO1:I=1-STRIG(0):POK.77,0:N.I:GR.10

    Line 2 is entry to the title screen, main loop point for level select, and sets the graphics mode for rendering after leaving the title screen.

    • CX=Q(L):CY=Q(L+12)The center of the Mandelbrot rendering CX, CY are read from 12 element portions of the constants array Q offset by the Location index L. These are the center coordinates for each preset location. (The Zoom parameter is read elsewhere)
    • D=64 Delta top D is set to 64 which will start the multi-resolution renderer rendering 64 pixel rows 64 pixels apart in the y (vertical) direction.
    • D2=D*2 D2 is set equal to D so the distance between rows is the same as the pixel height for the first pass, filling the screen.
    • H=D-1 Pixel height H is set to D-1 to account for drawing counting from zero. Drawing from 0 to 63 will draw 64 pixels, any more and we will get errors for trying to draw off the screen.
    • GR.18 is Graphics mode 2+16. Mode 2 is large text, and the +16 means fill the screen with text (do not include a 4 line window of normal Graphics mode 0 characters).
    • I=L*16+1 Calculate substring index I based on integer location index L. N$ will be filled with 16 character names, and Atari BASIC strings index from 1. L*16+1. The first time line 2 is executed L is guaranteed to be zero.
    • ?#6;N$(I,I+15),,,"x";CX,"y";CY,"z";Z
      ?#6 means print to device number 6 which is how to display text in graphics mode 2. Semicolons print strings back to back and commas insert tabs up to 10 spaces (two commas fill a 20 character mode 2 line). The 16 character location name sub-string is printed from N$ at index I followed by a comma to complete the line and two more commas to add a blank line. Then “x” and CX, then a comma (tab), “y” and CY, another comma (tab) and “z” and z. The lengths of these numbers are not controllable so we allow line is allowed to end Z after z.
    • ?#6,,,,"press JOY BUTTON"
      The next print includes four commas to skip two lines then prints the mixed case "press JOY BUTTON". In mode 2 all characters are printed in uppercase, printing mixed upper and lower case prints in different colors. Numbers and punctuation are printed in the same color as upper case.
    • F.I=0TO1:I=1-STRIG(0):POK.77,0:N.I
      Wait for the user to press the joystick button while resetting the Atari attract mode timer to prevent color cycling.
      • STRIG(0) is the trigger of the first joystick. It is 0 when pressed and 1 when not pressed.
        This will loop until I is 1 but 1-STRIG(0) will only be 1 when STRIG(0) is zero when the button is pressed.
      • POK.77,0 will reset the Atari computer attract mode timer preventing the automatic attract mode color cycling intended to protect CRTs in Televisions from image burn in.
    • GR.10 Sets the GTIA graphics mode 10, 80 x 192 pixels capable of 9 colors. Color 0 is set to black and is used for the border and pixels requiring more than the 81 Mandelbrot Calculations this program is capable of.

    3N$="mandelbrot set lightning seahorse valley spirals needle (mini) lightning (mini)scepter valley elephant valley needle hairs thorny lightningtails on tails @spine blossom ":IF D<=1 THEN G.6

    Line 3 sets all the location names in N$ and skips rendering if the image is completely rendered. Line 3 is executed right after title screens, with each iteration of the multi-resolution renderer, and when the image is fully rendered with each iteration of color cycling.

    • N$="mandelbrot set lightning seahorse valley spirals needle (mini) lightning (mini)scepter valley elephant valley needle hairs thorny lightningtails on tails @spine blossom " Sets N$ to the full 192 character string containing the 16 character names of the 12 locations in order. This could not be done earlier due to line length limitations.
    • IF D<=1 THEN G.6
      D is delta Y for each line in the multi-resolution renderer. The last pass will render every odd line with a D of 2 (even lines were rendered in previous passes). Each iteration D is divided by 2, when it reaches 1 rendering is done so rendering is skipped, GOTO 6 jumps to color cycling.

    4F.Y=T TO191STEPD:B=CY+(Y-96)/4*Z:F.X=0TO79:A=CX+(X-40)*Z:C.1:PL.X,Y:R=0:I=R:R2=R*R:I2=I*I:F.C1=0TOMC-1:J=R2-I2+A:I=(2*RI)+B:R=J:R2=RR:I2=I*I:C=C1:IF(R2+I2)<4THENN.C1

    Line 4 is the main part of the renderer setting up the nested pixel location loops for Y then X, plotting the cursor, and performing the iterative Mandelbrot calculation

    • F.Y=T TO191STEPD Loop over all the scan lines in the image starting at top T to the last line 191 stepping delta D which starts at 64 and is halved in each rendering pass
    • B=CY+(Y-96)/4*Z Transform screen pixel coordinate Y to imaginary (Y) coordinate in Mandelbrot coordinate system B. (A,B are used for real and imaginary coordinates because R and I are used elsewhere)
      • CY is the y coordinate of the center of the location in Mandelbrot space (on the imaginary y axis)
      • Y-96 centers y on the 192 pixel tall screen (range from -96 to 95)
      • /4 The divide by 4 is to correct for the wide 4:1 aspect ratio of Graphics 10 pixels (80×192 on a 4:3 aspect display)
      • *Z scales the Y range from -96 to 95 down to the desired zoom level. Small Z values effectively shrink the viewport which magnifies the apparent shape
    • F.X=0TO79 Loop over all 80 columns on a line. All lines in an area of an image take approximately the same time to render based on the complexity of the Mandelbrot calculations.
    • A=CX+(X-40)*Z Transform screen pixel coordinate X to real (X) coordinate in Mandelbrot coordinate system A. (A,B are used for real and imaginary coordinates because R and I are used elsewhere)
      • CX is the x coordinate of the center of the location in Mandelbrot space (on the real x axis)
      • X-40 centers X on the 80 pixel wide screen (range from -40 to 39)
      • *Z scales the X range from -40 to 39 down to the desired zoom level. Small Z values effectively shrink the viewport which magnifies the apparent shape
    • C.1:PL.X,Y Draw the cursor Color 1 plot X,Y. This gives the user an idea of where calculation is being performed and how long until the image may be fully rendered.
    • R=0:I=R:R2=R:I2=R Set real, imaginary, real squared, and imaginary squared variables to zero. Setting R to zero and other variables to R is an Atari BASIC optimization to fit more on a line.
    • F.C1=0TOMC-1 Calculation count C1 loop from 0 to MC-1, or loop Mandelbrot Calculations (MC) times.
    • J=R2-I2+A:I=(2*RI)+B:R=J: Perform Mandelbrot calculation
      • The Mandelbrot calculation is Zn+1 =Zn2+C in the complex plane.
      • Z and C are complex numbers
        (x, y*i) where i is the square root of -1
      • Z starts as 0 (R=0:I=R)
      • C is the location (A, B*i) derived from the viewport coordinates centered on CX, CY with a viewport scaled by Z (small values magnify)
      • Each iteration performs the complex calculation Zn+1 =Zn2+C
        J=R2-I2+A Calculate the real portion ZRn+1 = ZRn2 – ZIn2+CR
        R2 is precalculated ZRn2 from previous loop
        I2 is precalculated ZIn2 from previous loop
        A is CR
        J is a temporary variable needed so we can complete the imaginary portion of the calculation
        I=(2*R*I)+B Calculate the imaginary portion 2*ZRn* ZIn+CI
        R is ZRn
        I is ZIn
        B is CI
      • R=J Assigns the calculated real portion to R from the temporary variable J
    • R2=R*R:I2=I*I Calculate ZRn2 and ZIn2
    • C=C1 Save C1 into C for color cycling. At the end of the for loop C1 will be 1 more than the max value, which will cause us out of bounds headaches so we will use the last value saved here in C.
    • IF(R2+I2)<4THENN.C1 Test for divergence
      The colored shapes of the Mandelbrot plots you’re familiar with represent the how many iteration it takes before the calculation is divergent (it heads off to infinity). The central areas (typically black) either are not divergent, or require more calculations to resolve the infinite complexity of the boundary of the Mandelbrot set.
      It’s proven that Z will be divergent if the magnitude of Z is ever greater than 2. The magnitude of a complex number is calculated as the square root of the sum of the squared real and squared imaginary coefficients: sqrt(ZR2 + ZI2)
      Here we calculate the square of the magnitude and test if it is greater than the square of two; it is an optimization to remove a costly square root calculation.
      If the function is not yet divergent ( (ZR2 + ZI2)<4 )continue to loop with NEXT C1

    5C.Q(C+78):PL.X,Y:DR.X,Y+H:D.0, -0.170337, -0.745107298, -.462160300, -1.6487359367, -0.17485, -1.287769, .3150751, -1.9990959, -1.192101, -0.747227, -1.457758

    Line 5 Set the color based on the calculated complexity, plot the top of the pixel, and draws a vertical line to fill the multi-resolution line. The rest of the line is filled with data.

    • C.Q(C+78) Set color based on complexity C which is the loop iteration count. from line 4 used to index precalculated color cycling table stored in Q in line 1
    • PL.X,Y:DR.X,Y+H Plot the top of the pixel X,Y and draw a line to the bottom of the pixel X,Y+H
    • D.0, -0.170337, -0.745107298, -.462160300, -1.6487359367, -0.17485, -1.287769, .3150751, -1.9990959, -1.192101, -0.747227, -1.457758
      This data is the X (or real) coordinates of the 12 preset locations.

    6F.I=1TO8:POK.704+I,PEEK(705+I):N.I:C=PEEK(713)+Q(36+DC):C=C-INT(C/256)*256:POK.713,C:POK.77,0:S=STICK(0)-5:IF S=10 THEN G.8

    Line 6 Performs color cycling, resets attract mode timer, checks for stick input, skipping line 7 if there is none.

    • F.I=1TO8:POK.704+I,PEEK(705+I):N.I copy 7 colors “down one” by looping through color locations 1 to 8 (counting from memory location 704 which is the base for all colors in memory). and setting them with data from locations 2 to 9 respectively.
      Colors for GTIA graphics modes, including mode 10 must be read and set by peek and poke. Atari BASIC was written to support the earlier CTIA graphics chip and does not provide commands to set or read more than 4 colors. (See “Graphics 10” on GTIA: An Illustrated Overview)
    • C=PEEK(713)+Q(36+DC):C=C-INT(C/256)*256:POK.713,C Perform color cycling on color 9 in memory location 713 (704+9).
      • C=PEEK(713)+Q(36+DC) Read color byte from color memory location and add a constant to it to implement color cycling.
        DC is the index to the type of color cycling which is determined by the constant that is added to the color register each iteration.
        The 9 value color cycling constants table starts at index 36 in Q
        Atari computers are capable of displaying 128 colors which are encoded into a bite as follows:
        The upper 4 bits represent the 16 possible hues
        Even values of the the lower 16 bits represent 8 intensities.
      • C=C-INT(C/256)*256 Atari BASIC lacks modulo and boolean operations. This calculation is equivalent to modulo 256 and this pattern is used frequently in my BASIC programs. See Modulo below for a description of how it works.
      • POK.713,C Store the adjusted color value back in color 9 in memory location 713 (704+9)
    • POK.77,0 Reset attract mode timer to prevent unwanted color cycling
    • S=STICK(0)-5:IF S=10 THEN G.8 Read Joystick 0 value and skip joystick processing if the stick is in the unmoved centered position.
      Tha Atari joysticks return the following values when pushed in the respective directions (15 center):
       10     6
     11   15    7     Atari Joystick values
        9     5    

    By subtracting 5 we get these values which are smaller indices for table lookups later. Notice that the center value is 10, so if S=10 then we skip joystick processing by GOTO 8

        5     1
     6    10    2     Atari Joystick values -5
        4     0    


    Line 7 The joystick moved either changing a location or a zoom level. Make a cute joystick acknowledgement chime sound, adjust location, adjust zoom level, partially reset multi-resolution renderer, pop out of X and Y loops and go to Title screen

    • F.I=24TO0STEP-1:SO.0,I*.4+24,10,I*.75:N.I Make chime sound by looping from 24 to zero and performing a sound statement with the following parameters:
      0 voice 0 (first of 4 voices)
      I*.4+24 Pitch range from 33 to 24 (increasing pitch)
      10 Distortion, 10 is pure square wave
      I*.75 Volume 18 to 0, decreasing from full volume to silence.
    • L=L+Q(45+S):L=L-INT(L/12)*12 Add delta level to L from constants starting at the 45th element of Q indexed by joystick direction S which is stick direction -5 (and remember S=10 skips this line). Here are the table values. 1,1,1,0,11,11,11,0,0
      Indices 0,1,2 all move the stick to the right to the delta level is 1
      Indices 4,5,6 all move the stick to the left so the delta level is 11. The L=L-INT(L/12)*12 modulo 12 operation only works on positive values so in order to “decrement” the level 11 (12-1) is added and then the modulus operator works.
      Indices 8 and 9 move the stick up and down which are zoom functions that do not impact level so delta level is 0
    • Z=Z*Q(56+S)+Q(L+24)*Q(67+S)
      Set Zoom level. This operates similarly to the level select using table lookups but must be able to reset Z when a new level is selected (left or right on the joystick) or half or double Z when zoom is adjusted (up or down on the joystick)
      Atari BASIC lacks ELSE and ENDIF so each IF THEN statement completes a source code line requiring new approaches to solve basic problems when line count is limited. HERE we use the numeric method of choosing coefficients based on stick direction to solve this problem in one equation and table presets.
      Q(L+24) Is the initial Z value for the level L, 12 initial Z values are stored in Q starting at index 24
      Q(56+S) Is the coefficient for the existing Z value 2,.5,0,0,2,.5,0,0,2,.5
      Notice these are 2 in all downward direction indices, .5 in all upward direction indices, and zero for left and right
      Q(67+S) Is the coefficient for the default Z value for the current level L
      Notice these are 1 in all left and right direction indices and 0 in up and down
      The result is that pushing up, down, left, or right results in one side of the plus sign in the equation being zero and the other having a meaningful value
    • T=0 Reset the top of the multi-resolution pixel to 0
    • DC=T Reset color cycling type to 0
    • POP:POP:G.2 Pop out of X and Y loops and goto line 2 to display the Title screen for the current location and zoom level.

    8IF STRIG(0)=0 THEN SO.1,20,10,15:SO.1,20,10,0:F.I=0 TO 1:I=STRIG(0):POK.77,0:NEXT I: POK.713,11:DC=DC+1:DC=DC-INT(DC/9)*9

    Line 8 reads the joystick button while rendering and if pressed acknowledges with a chirp and cycles through the 9 color cycling modes.

    • IF STRIG(0)=0 THEN Tests to see if the button is pressed, STRIG(0) is 0 when pressed
    • SO.1,20,10,15:SO.1,20,10,0 Emits a chirp on voice 1, frequency 20, distortion 10 (pure square wave), volume 15 then immediately 0 in the second command
    • F.I=0 TO 1:I=STRIG(0):POK.77,0:NEXT I Loop similar to one used in line 2, but this one waits for the button to be released (STRIG(0) = 1). POKE 77,0 again in the loop to reset attract mode timer.
    • POK.713,11 Set color value to a known value so cycling is deterministic (not dependent on the value in the color register when button is hit)
    • DC=DC+1:DC=DC-INT(DC/9)*9 Increment color cycling type index DC and perform modulo 9 on the value.

    9IF D>1 AND S=10 THEN N.X:N.Y:D2=D2/2:D=D2:T=D/2:H=T-1:D.0, -1.06506, -0.15020534, -.5823998, 0, -1.071623, .063761, .02918, -0.000000063, 0.3061769, 0.105766, -0.001353

    Line 9 if renderer is active then continue looping through all pixels on the screen, then adjust the multi-resolution renderer for the next pass. Line contains data statement with Y values for 12 preset locations.

    • IF D>1 AND S=10 THEN If delta line D is two or greater and the stick is not pushed in any direction then the renderer is still active so…
    • N.X:N.Y Finish looping through pixels in the current pass of the renderer then (still in the IF THEN…)
    • Reset multi-resolution renderer for next pass
    • D2=D2/2 Divide D2 by 2
    • D=D2 Set D to D2
    • T=D/2 Set the top of the next pixel to half the delta D (fills bottom half of previous row)
    • H=T-1 Set row height to be the top index (halfway down previous row) -1. The minus 1 is to account for starting drawing at pixel offset 0, so 0 to T-1 actually covers T pixels exactly. (without it there would be draw out of bounds error.
    • D.0, -1.06506, -0.15020534, -.5823998, 0, -1.071623, .063761, .02918, -0.000000063, 0.3061769, 0.105766, -0.001353
      Data statement containing Y values for 12 preset locations.
      Note: The DATA statement is not impacted by the IF THEN statement.

    10.G.3:D.0.05, .000001, .000002, .0001, .000001, 0.000045, .000004, .0004, .00000003, .00001, 0.000147, 0.00002, 254, 2, 32, 14, 18, 16, 238, 242, 240, 1, 1, 1, 0, 11, 11, 11, 0, 0, 0, 0, 2, .5, 0, 0, 2, .5, 0, 0, 2, .5, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0

    Line 10 is the final loop statement for the renderer and the data statement containing the remainder of the preset constants needed by the program

    • G.3 Forever loop that contains the multi-resolution renderer, color cycling, and user input.
    • D.0.05, .000001, .000002, .0001, .000001, 0.000045, .000004, .0004, .00000003, .00001, 0.000147, 0.00002, 254, 2, 32, 14, 18, 16, 238, 242, 240, 1, 1, 1, 0, 11, 11, 11, 0, 0, 0, 0, 2, .5, 0, 0, 2, .5, 0, 0, 2, .5, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0
      Data statement containing:
      • Zoom levels for 12 preset locations (12)
      • Color cycling deltas for 9 color cycling modes (9)
      • Z multipliers for joystick direction lookup (11)
      • Level default Z multipliers for direction lookup (11)

    Special features of this program

    GTIA Graphics 10

    Atari computers originally shipped with the CTIA chip only capable of displaying a background and up to three foreground colors from a 128 color palette (16 hues each with 8 intensities)

    Colors - Atari 8-Bit Computers - AtariAge Forums

    The GTIA chip released shortly after the Atari 400/800 were released offered three new graphics modes:

    • Graphics 9 provides 16 intensities of any hue
    • Graphics 10 used by this program uses all of the background (1), foreground (3), player (4), and missile (1) color registers to provide 9 color bitmapped graphics with programmable color registers
    • Graphics 11 provides 16 hues at any singleintensity
    • For more information see GTIA: An Illustrated Overview

    Memory locations (PEEK/POKE)

    704-712 Color registers

    • High 4 bits 16 hues, lower 4 bits 8 intensities (low bit is ignored, see image above for 128 color palette of Atari computers).
    • Atari BASIC has no command to read a color and SETCOLOR is only able to set the background and 3 foreground colors.
    • The only way to read and write GTIA Graphics mode 10 colors is by using PEEK and POKE respectively to access locations 704 to 712.

    77 attract mode timer

    • The Atari computers have a built in “Attract Mode” that automatically begin cycling colors of the display. The term “Attract Mode” refers to attracting people to interact with the computer in a store, the theory being that the color changing would get peoples attention more than a static display. The Attract Mode was also needed to prevent CRT burn in. When a television displays the same image for a long period of time the phosphor in the display can be permanently marked. The color cycling in the attract mode prevents this both for in-store and home TV’s.
    • An interactive Atari BASIC application can disable the automatic Attract Mode by frequently resetting the internal Attract Mode timer to 0 with a POKE 77,0
    • For more technical details see location 77 in Atari Computer Memory Map

    No ELSE, no ENDIF

    Atari BASIC has IF <expr> THEN <expr>:<expr>…
    But is has no ELSE, nor ENDIF statements. This means that the remainder of any line after an IF THEN statement will be committed to the conditional statement; all statements after THEN will only be executed if the expression is true.

    This is especially limiting in this kind of contest where the program may only have 10 lines. Here are strategies I used to work around this limitation:

    1. Use lookup tables indexed by joystick input to selectively change levels or zoom.
      See L=L+Q(45+S)in line 7
      S is an index derived from joystick 0 direction, some values in the Q data table are 0 which will not change the level (joystick center, up, down), some are +1 (joystick right) to increment the level, and some are +11 (joystick left) which when used with the modulo function below effectively decrement the level.
    2. Use algebraic strategies to weight calculations based on data lookup rather than perform IF THEN conditional statements. This strategy calculates the answer for multiple conditions and then multiplies each result by coefficients of 0 or 1 and sums the results. (A strategy I’ve also used in graphics shaders, and vector processing)
      See Z=Z*Q(56+S)+Q(L+24)*Q(67+S) in line 7
    3. Use Modulo function (see below) to clamp integer values to ranges
      • 256 for color cycling values C=C-INT(C/256)*256 in line 6
      • 12 locations L=L-INT(L/12)*12 in line 7
      • 9 color cycling modes DC=DC-INT(DC/9)*9 in line 8
    4. Use GOTO at the end of IF THEN to skip over lines, effectively an ELSE. See lines 3 and 6.
    5. Fill ends of conditional statements with DATA which is not impacted by flow control statements like IF THEN or GOTO.

    Modulo function

    A modulo or modulus function returns the remainder of a division operation. For example MOD(A,B) would return the remainder of A/B, so for A=10, and B=5 would return 0, for A=10 and B=3 would return 1. (limiting to integers for simplicity here)

    The example most often used to illustrate modulus math is a clock. You can keep adding hours to a clock and eventually as the hour hand passes midnight it will reset to 0. No matter how many hours you add to a clock, when you read it the hour hand it will always display a value between 0 and 12 hours, which represents the modulus of the total number of hours and 12 which is the same as remainder of the total number of hours divided by 12.

    A modulo operator is really handy when programmatically cycling through a limited number of values (like menu options) or performing a math operation and bounding the output value (like a color register) because if the inputs are positive integers then the output will be an integer between 0 and the numerator minus 1.

    Atari BASIC lacks binary logic operations AND, OR, XOR, and a modulo function. Binary logic operations can perform fast modulo operations on numbers that are a power of 2 by masking bits out of range. For example, MOD(8,B) is the same as B AND (8-1). The explanation is beyond the scope of this article but it is FAST and really handy.

    I’ve always used this technique in Atari BASIC to calculate a modulo operation on positive integers.
    Let’s break it down from the inside out.

    A/B will be < 1 if B<A
    A/B will >=1 if B>=A

    INT is the integer operator with in Atari BASIC always rounds down to the nearest integer less than the value provided

    INT(A/B) will be 0 if B<A
    INT(A/B) will be a positive integer if B>A. That integer is the number of times B can be subtracted from A

    INT(A/B)*B will be 0 if B<A
    INT(A/B)*B will be a positive integer if B>A. This integer is the greatest multiple of B less than A.

    So, A-INT(A/B)*B will subtract from A the portion of A that is from multiples of B, leaving the remainder of A/B

    This modulo calculation is used in the program to wrap three values:

    • The number of locations to 12: L=L-INT(L/12)*12 in line 7
    • the number of color cycling modes to 9: DC=DC-INT(DC/9)*9 in line 8
    • and the color register to 256 (the size of a byte): C=C-INT(C/256)*256 in line 6

    Since this approach works for positive integers, how does the program decrement values? It adds the number of elements minus 1.

    Using a clock as an example: If it is 5 o’clock:

    • If we add 1 hour it will display 6 o’clock
    • If we add 12 hours the hand will spin all the way around and it will display 5 o’clock: adding the modulus is the same as adding 0
    • If we add 11 hours the hands will spin almost all the way around but will be short by 1 hour and it will display 4 o’clock: subtracting values are the same as adding the modulus the value, in this case 12-1.

    This program uses the third point, subtracting a value is equivalent to adding modulus minus that value in order to implement wrapping adds in location selection, color cycling mode selection, and color cycling itself.

    Managing program lookup tables and constants

    This program uses a lot of data that needs to be indexed like tables, some is precomputed at initialization and some is predefined. To keep the program as short and simple as possible all the constants are stored in a single array called Q. Line 1 initializes Q by performing precomputation of color table values and READing predefined data from DATA statements. The DATA statements are spread throughout the program but are read in order.

    The data is accessed many places in the program by referencing Q(base_index+offset). For example in line 2 CY=Q(L+12) is loading the center y coordinate CY from the Q array at index 12 plus the current location L. If you look in the contents below you will find the offset to the center Y location table is at offset 12 and the table contains 12 elements.

    Data array Q contents:

    0 (12 elements) Center X coordinates for 12 program locations
    12 (12 elements) Center Y coordinates for 12 program locations
    24 (12 elements) Default Zoom for 12 program locations
    36 (9 elements) Delta Color for each of the 9 color cycling modes
    45 (11 elements) Delta Level indexed by joystick 0 direction
    56 (11 elements) Z multiplier (0.5,2) indexed by joystick 0 direction
    67 (11 elements) Initialization (0,1) Z multiplier indexed by joystick 0 direction
    78 (81 elements) Color to plot indexed by number of Mandelbrot calculations

    How I developed this program

    Atari BASIC automatically expands abbreviations and artificially short line lengths are enforced by the built in editor. The way to work around this limitation is to edit the program externally as a text file and use the ENTER command to read the text file into the interpreter (as opposed to SAVE and LOAD which create and read an Atari BASIC tokenized file). The Atari800Win-PLus emulator allows you to map a folder on the host as device H: (Atari menu, Settings option, Enable H: patch for hard disk access).

    The next challenge is that Atari uses 155 (0x9B) as a line terminator and fails to read text files with conventional carriage return (CR, 13, 0x0D) or linefeed (LF, 10, 0x0A) line terminators. To discover this I used the HxD hexadecimal editor to inspect program listings I made in Atari BASIC.

    To develop the program, I edited a conventional text version of the program in Notepad++. To test the program I copied the text to MANDELBR.LST, replaced the line endings, saved the file, and used ENTER"H:MANDELBR.LST" in the emulator to load the program. This process was iterated (a lot!).

    When the program was complete I took the following steps to create the contest disk image:

    • Booted the emulator with a blank Atari DOS 2.0S image called Mandelbrot Set Explorer – 10 lines Atari BASIC.atr
    • From DOS used option C COPY FILE to copy H:MANDELBR.LST to D1:MANDELBR.LST
    • From DOS used option B RUN CARTRIDGE to exit to BASIC
    • Typed ENTER"D1:MANDELBR.LST" to load the program
    • Typed RUN to verify that it works, then Break to stop program execution
    • Typed SAVE"D1:MANDELBR.BAS" to save an Atari BASIC format file

    References for programming Atari Computers

    Mandelbrot Set Projects New And Old

    I’m working on a followup project composing deep dive videos from 297 color cycling images generated by this program. Atari Chiptune artist Adam Sporka has generously agreed to to provide all the music for these videos. They are turning out awesome, even though they are composited with modern software on modern computers, it’s amazing to see and hear every pixel and sound that was originally generated on Atari computers.

    It’s done! The videos, playlists, and details are all here: Atari 8 Bit Mandelbrot Set Zoom Videos

    Other Mandelbrot Set related projects…

    I’ve explored the Mandelbrot Set in a couple previous projects. The Python ASCII Mandelbrot project …

    and VT100 terminal Mandelbrot project…

    Fast (~2 minutes)

    Real-time (~12 minutes)

    The source code is included in the YouTube video descriptions. I also wrote a Python VT100 Mandelbrot set explorer (not pictured here) that was configured to match the capabilities of this Atari BASIC program and used to pinpoint the 12 preset locations.

    Special Thanks

    I’d like to give a shout out to my local makerspace Workshop 88, a like minded talented creative community active in the Chicago suburbs and accessible throughout the world. Be sure to check out the rest of this blog, our YouTube Maker Meeting videos, an all our socials (Facebook, Twitter, MeetUp…). You can join (or support) our community from anywhere in the world on Patreon.

    I’d like to like to recognize and thank my father Dennis Williamson, and uncles Don Bahr, Henry Bahr, and Tom Mammoser for fostering my early interest in computers, electronics, and Atari as well as my grandparents Herbert and Virginia Williamson for my first Atari 400 computer and unlimited love, support, and encouragement.

    I had a lot of fun writing this program, writing this article, and on the upcoming Atari Mandelbrot Set deep dive video project. In writing the detailed description of the code I discovered a few minor bugs, see if you can find them!

    Maker Meeting December 29, 2020

    Check out the December 29, 2020 Workshop 88 Maker Meeting here: https://www.youtube.com/watch?v=D6PLhZxu954

    For details (including recipes and pictures) see details on the December 29 Workshop 88 Maker Meeting page:

    We have great things planned for the first meeting in 2021 including the design, machining, and construction of Tom’s spring loaded engraver and a considerable discussion on the topic of organization!

    Workshop 88 is a makerspace in Glen Ellyn Illinois. We are more than a workshop, we are a growing community of creative talented people who aspire to learn and share knowledge, experiences, and projects.

    Join us! To become a member join at Workshop88 or you can help us continue to share our projects and activities by supporting us via Patreon.

    Never miss a tip or project! Follow our blog at www.Workshop88.com, subscribe to Workshop88’s YouTube channel, like us on Facebook, follow us Twitter and join or support our maker community by contributing to Workshop88 on Patreon!

    To find out about upcoming events follow Workshop88 on Meetup.
    Have a question? email us at info@Workshop88.com

    Maker Meeting December 22, 2020

    Check out the December 22, 2020 Workshop 88 Maker Meeting: https://www.youtube.com/watch?v=Y_iZNCfRGe8

    This week’s episode featured:

    More details and links can be found on the December 22, 2020 Maker Meeting Workshop 88 page.

    Workshop 88 is a makerspace in Glen Ellyn Illinois. We are more than a workshop, we are a growing community of creative talented people who aspire to learn and share knowledge, experiences, and projects.

    Join us! To become a member join at Workshop88 or you can help us continue to share our projects and activities by supporting us via Patreon.

    Never miss a tip or project! Follow our blog at www.Workshop88.com, subscribe to Workshop88’s YouTube channel, like us on Facebook, follow us Twitter and join or support our maker community by contributing to Workshop88 on Patreon!

    To find out about upcoming events follow Workshop88 on Meetup.
    Have a question? email us at info@Workshop88.com

    Maker Meeting December 16th, 2020

    Workshop 88 Maker Meeting, December 16th, 2020: YouTube link

    This Week’s Topics & Discussions

    Meeting notes

    About Workshop 88

    Workshop 88 is a makerspace in Glen Ellyn Illinois. We are more than a workshop, we are a growing community of creative talented people who aspire to learn and share knowledge, experiences, and projects.

    Join us! To become a member join at Workshop88 or you can help us continue to share our projects and activities by supporting us via Patreon.

    Never miss a tip or project! Follow our blog at www.Workshop88.com, subscribe to Workshop88’s YouTube channel, like us on Facebook, follow us Twitter and join or support our maker community by contributing to Workshop88 on Patreon!

    To find out about upcoming events follow Workshop88 on Meetup.
    Have a question? email us at info@Workshop88.com

    Melamine or chipboard drawer face repair

    Last week I shut one of my shop drawers loaded with sandpaper and manuals with a too much “gusto” which ripped the drawer face off and left large deep pits in the chipboard around the mounting screws. Here is how I fixed it.

    Aw shucks!

    This is the broken drawer, you can see how the 4 mounting screws ripped deep holes out of the Melamine chipboard drawer front.

    For this repair you will need:

    • Eye protection
    • Pen or pencil
    • Ruler (preferably long)
    • Saw, container, and sieve (or sawdust)
    • Wood glue
    • Wire brush (optional)
    • Clamps (screw clamps and hand clamps used in the example)
    • Scrap wood to protect your surfaces from clamps
    • Screwdriver suitable for your existing drawer screws
    • Paper (sticker backing, baking parchment, or waxed paper is best)
    • Popsicle stick or similar spreader
    • Disposable cup or other container
    • Sharp chisel or sandpaper (optional)

    Wearing eye protection, clean out the holes. You can compressed air, or you can pucker up and blow them out with your own compressed air but be sure to close your eyes and wear eye protection, sawdust and debris is nearly guaranteed to blow right into your eyes with this method (voice of experience). Alternatively you can gently clean them out with a brush with the holes facing downward.

    Next, mark lines through the centers of the screw holes around each pit. This step is important for getting the screw holes located later in the repair so the drawer front is aligned and spaced properly so take your time. I found using a long ruler lined up with the centers of pairs of holes horizontally, vertically, and diagonally to mark the area around each repair worked well. If you use a permanent marker as I have take note which areas of the back of the drawer face will be visible when reassembled. It may be useful to look at other drawers to get an idea of what areas will be visible. If you use a pencil or erasable marker (dry erase, grease pencil) be careful not to erase your marks later when working in the area.

    Next you will need sawdust. I grabbed a piece of scrap plywood, clamped it in the vice and started cutting from the end. After 3 cuts I felt I had enough. You want the sawdust pretty fine, no chunks or splinters so I sieved it.

    Now it’s time to make our own chipboard filler and fill the holes. I mixed up a little at a time so I could get the consistency I wanted and so the batch wouldn’t get too gummy while working. Start with glue in a disposable container. I used a plastic cup but it can be any container as long as it is clean. I start with glue, add sawdust which will absorb some of the moisture of the glue and mix. I keep adding glue or sawdust until the mixture is a putty like spreadable consistency. You want it on the wet side so the glue can bond to the chipboard in the next step.

    Working one hole at a time using the Popsicle stick or spreader, start pressing the filler into a hole. You want to pack it as deep as you can into the bottom without leaving voids or bubbles. The glue will shrink as the moisture evaporates during curing so mound the holes full.

    Now we are going to apply some pressure with clamps. Select pieces of scrap wood to protect your surfaces from the clamps which will dent, scratch, and break the surfaces of your drawers if they are not protected. I used screw clamps, you can use whatever you have available.
    Place a piece of paper over each glue filled hole. If your paper has a shiny side that side should face the glue. Place smooth wooden blocks large enough to more than cover the hole over the paper. With a second wooden block on the other side tighten each clamp. The clamp should be snug to tight, you don’t need to crush the wood, just hold it tightly together.

    Leave for a two to three hours to let the pressure distribute the materials and the glue will penetrate the existing chipboard.

    The glue will not cure if covered so after several hours remove the clamps and carefully remove the paper to leave as much filler as possible in the holes. Leave them overnight to dry or longer to dry and cure. They will likely take longer to cure than wood glue usually takes due to the volume of the filler.

    You will probably notice that the old screw threads are full of wood. Clean them with a wire brush so they bite into the repaired drawer face without interference. If you don’t have a wire brush you may be able to clean them using the drawer body front by backing them all the way out. This step is not critical.

    The next day or when you are confident all the filler has fully cured you can proceed. The filler likely contracted leaving the center slightly concave, this is OK. If you have a large void and feel you need more filler you can repeat the above steps but it is usually not necessary.

    Remove any excess glue or filler on the drawer face. A sharp broad chisel worked very well for me. You can try a razor scraper or sandpaper. Assuming you don’t have any large lumps this step is optional to assure a snug smooth fit between surfaces.

    Select an appropriately sized pilot hole drill bit. I typically do this by holding candidate bits behind and in front of the screw. For woodworking in solid wood I would normally select a slightly smaller bit but considering my experience with chipboard, I selected a bit that was nearly the diameter of the center of the screw leaving plenty of material for the threads to bite into.

    This is a good time to create a depth gauge by wrapping your drill bit with a piece of painters tape to indicate how long the screws are (not pictured). This will allow drilling holes deep enough for the screws without drilling all the way through to the face of the drawer.

    Using a ruler and sharp pencil, re-draw the lines to determine where the screw centers should be.

    Carefully drill pilot holes at the screw center locations. Make sure the drill holes are perpendicular to the drawer face (not angled). Again, be careful to drill the holes deep enough for the screws but not deep enough to damage the front of the drawer face on the other side.

    Back up the screws so that only the points protrude and get some hand clamps ready. We will use the screw tips to align the drawer face on the drawer and hold it in place with the clamps.

    Place the drawer front on the drawer carefully locating each screw tip in one of the pilot holes you drilled. Once aligned place the hand clamps and take a moment to verify the screws are in the holes and the drawer face looks properly positioned and aligned. Take your time on this step.

    Gently tighten the screws. I give each screw a couple turns with a hand screwdriver and move on to the next until they are all fully screwed in. Don’t over torque the screws, you know what they’re biting into. I advise tightening these by hand with a manual screwdriver. If you choose to use an electric screwdriver, use slow speed (screwdriver, not drill), the lowest torque setting, and still do the final tightening with a hand screwdriver.

    There are a couple optional things you can do for additional strength:

    • You could drill pilot holes and add more screws into fresh material. Again, be careful to select appropriate size/length screws so you get a good bite without drilling or screwing through the face of the drawer.
    • You could apply epoxy between the faces before screwing them together for a permanent bond but you better get everything aligned perfect because there is no undoing that. (not advised)

    Perfect fit! Which one was repaired?

    The top drawer, ready to be re-filled.

    Clean up the shop. 😉

    I hope this was helpful and you picked up a tip or two.

    Stay tuned here on the Workshop88 blog for more articles and updates, subscribe to Workshop88’s YouTube channel for our regular Maker Meetings and other content, like us on facebook, join or support our maker community by contributing to our Patreon (we could really use your help!).

    D. Scott Williamson
    Compulsively Creative

    Workshop 88 Weekly Maker Night – November 17, 2020


    Workshop 88 is a makerspace in Glen Ellyn Illinois. We are more than a workshop, we are a growing community of creative talented people who aspire to learn and share their insights, experiences, and projects.

    Please subscribe to Workshop88’s YouTube channel, like us on facebook, and join or support our maker community by contributing to Workshop88 on Patreon!

    To find out about upcoming events follow Workshop88 on Meetup.
    Do you have a question? email us at info@Workshop88.com

    YouTube link: Workshop 88 Weekly Maker Meeting – Episode 3

    Welcome to the Workshop 88 Makers Round-table on Zoom where we share our projects, ideas, and related resources. The attendees and agenda are below, the meeting will be recorded and hosted on YouTube, and resources like links and other related information can be found here after the event.


    • Andrew Morrison
    • Scott Williamson
    • Jim Williams
    • Tom Matukas
    • Gail Jo Kelly
    • Daniil Bystrukhin
    • Peter Fales
    • Phil Strons
    • Karl Knutson
    • Christine Heermann

    Show & tell time!

    Each session will target 10 minutes for show and tell and 5 minutes for Q&A

    7:00pm – Scott will share his Schlieren photography setup. Schlieren photography allows you to visualize changes in air density and used to see airflow, temperature differences, shock waves, or sound.

    7:30pm – Rick will share his experiences with the STMicro accelerometer, magnetometer, and gyroscope leading into a discussion about creating a tilt compass using trigonometry.

    General discussion topics

    • How would you calibrate an anemometer?
      • Jim Williams: Drive a car with it out the window on a stick to get outside of the car’s air-stream and have a copilot record rpm and car speedometer. (Scott added: In both directions to factor out a gentle breeze)
      • Mark Frost: Put it on the end of a stick and spin at a known rate in a still room to know airspeed of the sensor.
      • Scott Williamson: Connect it to the end of a tube connected to a large plastic bag. Place the bag in a box of known volume, place a second slightly smaller weighted box on top of the bag within the first box to construct a piston. Measure the time it takes to move the known outer box volume of air through the tube of known diameter to estimate the airspeed. Repeat with different weights.
    • Andrew Morrison: Why does an accelerometer measure an acceleration when it is not moving or changing velocity?
    • Rick Stewart: Laser Nanny repair
    • Did not cover: SMD soldering tips – (flux, solder paste, iron, iron tip, hot air, temps…)

    Meeting notes

    Please subscribe to Workshop88’s YouTube page, like us on facebook, and join or support our maker community by contributing to our Patreon.

    Workshop 88 Weekly Maker Night – November 11, 2020


    Workshop 88 is a makerspace in Glen Ellyn Illinois. We are more than a workshop, we are a growing community of creative talented people who aspire to learn and share their insights, experiences, and projects.

    Please subscribe to Workshop88’s YouTube channel, like us on facebook, and join or support our maker community by contributing to Workshop88 on Patreon!

    To find out about upcoming events follow Workshop88 on Meetup.
    Do you have a question? email us at info@Workshop88.com

    Welcome to the second Workshop 88 Makers Round-table in a row on Zoom where we plan to share our projects, ideas, and related resources. The attendees and agenda are below, the meeting will be recorded and hosted on YouTube, and resources like links and other related information can be found here after the event.


    • Andrew Morrison
    • Scott Williamson
    • Jim Williams
    • Tom Matukas
    • Gail Jo Kelly
    • Daniil Bystrukhin
    • Peter Fales
    • Phil Strons
    • Karl Knutson
    • Christine Heermann

    Show & tell time!

    Each session will target 10 minutes for show and tell and 5 minutes for Q&A

    7:05pm – Scott recently explored Free and Open Source video editing software and will share his findings including resurrecting an antique and finding a real gem!

    7:20pm – Jim will share laser cut fluorescent non-round gears

    General discussion topics

    • Home Automation (ESP-01, ESP-32, Arduino, WiFi, MQTT, Mosquito, Raspberry Pi, Linux, battery/solar power…)
      • Daniil and Peter shared a lot of detailed information about the hardware, protocols, software they use to monitor and control many things around their homes – links below.
    • “What are you 3D printing?”
      • Andrew shared hexagonal tangram-like puzzle he designed in TinkerCAD and 3D printed. We discussed additional puzzles and ways to improve the puzzle and print quality.
    • “What are you making / preparing for the holidays?”

    Meeting notes

    Please subscribe to Workshop88’s YouTube page, like us on facebook, and join or support our maker community by contributing to our Patreon.

    Workshop 88 Weekly Maker Night – November 3, 2020

    YouTube link to this event


    Workshop 88 is a makerspace in Glen Ellyn Illinois. We are more than a workshop, we are a growing community of creative talented people who aspire to learn and share their insights, experiences, and projects.

    Please subscribe to Workshop88’s YouTube channel, like us on facebook, and join or support our maker community by contributing to Workshop88 on Patreon!

    To find out about upcoming events follow Workshop88 on Meetup.
    Do you have a question? email us at info@Workshop88.com

    Welcome to the second Workshop 88 Makers Round-table in a row on Zoom where we plan to share our projects, ideas, and related resources. The attendees and agenda are below, the meeting will be recorded and hosted on YouTube, and resources like links and other related information can be found here after the event.


    • Andrew Morrison
    • Scott Williamson
    • Jim Williams
    • Tom Matukas
    • Gail Jo Kelley
    • Rick Stewart
    • Breezy Fasano
    • Peter Fales
    • Phil Strons

    Show and tell time!

    Each session will target 10 minutes for show and tell and 5 minutes for Q&A

    7:00pm – Breezy will share her custom created keyboard as well as information and tips for making your own.

    7:15pm Rick will share part of his MQTT / NodeRED project for monitoring outside temperature and door activity (in progress). (LED word clock will be shared in a later meeting.

    730pm (Postponed)Scott recently explored Free and Open Source video editing software and will share his findings including resurrecting an antique and finding a real gem!

    7:45pm Phil will share antenna constructions (Ham & HD)

    Discussion topics

    8:00pm GailJo – Nintendo Switch Joystick repair followup

    Disassembly project tips: tweezers, pictures, screw containers… laptop failure

    Meeting notes

    Please subscribe to Workshop88’s YouTube page, like us on facebook, and join or support our maker community by contributing to our Patreon.

    Doorknob Handle

    Like many Americans, my family had a spring break vacation planned just as the Coronavirus pandemic began to spread in North America. For weeks while looking forward to spring break we had been watching the dramatic growth of COVID-19 and steps taken to contain and control the virus in China, and more recently in Italy, Iran, and South Korea. We decided to cancel our vacation and begin social distancing. While disappointed, it was immediately clear that we made the right decision for ourselves as we would have likely been stranded away from our home in the USA and for everyone else in this time of aggressive spread of a highly infectious disease that hospitalizes 1 in 10 and kills 1 in 50 people.

    We made our decision on Saturday the day before our flight was to leave, we all thought about social distancing and what we should do to protect ourselves and potentially others from us (two of us had recent colds and were uncertain if we caught it and symptoms were mild). Hand washing, not touching our faces, sanitizing everything around us were discussed, but when I found out that COVID-19 could last an astonishing 3 days on metal and plastic surfaces I immediately noticed all the doorknobs in our house. A doorknob you have to grasp with hour hand requiring a lot of contact, a door handle you can open with your elbow, or at least one finger. After you’ve been out in public touching door handles, railings, shopping carts, elevator buttons, ATM’s, money, gas pumps… You have to touch the exterior doorknob to enter the house before you are able to wash your hands, and everyone needs to touch it.

    I also felt like I wanted to do something to help. I decided to design and share a customizable 3D printable handle that could be easily and nondestructively be attached to doorknobs. Not only are they more sanitary, but they turned out to be immensely convenient when carrying in groceries, or walking out with a beverage, etc. Below are links to my models and source code on Thingiverse, along with the write-up that accompanies them.

    Updated stronger customizable doorknob handle with more options: https://www.thingiverse.com/thing:4228668
    Note: Thingiverse customizer does not work with this model, it may be too complicated. Download Knob handle v5.scad*, open in OpenSCAD, and enable the Cusomizer UI in the View menu.

    Original door handle: https://www.thingiverse.com/thing:4224517

    Unfortunately this was inspired by Coronavirus COVID-19

    If you think these may help you, your family, or others, please feel free to customize and print as many as needed.

    If you would like to manufacture and commercialize this product please contact me, we can work something out.

    Novel Coronavirus or COVID-19 is spread like cold and flu viruses via contact with droplets (sneezes, coughs) from an infected individual. It has been proven viable in the air for 3 hours and has been demonstrated to contaminate surfaces for up to 3 days where it may be picked up on hands. A potential vector for infection is touching your face (mouth, nose, or eyes) with the virus on your hands. Frequent thorough hand washing with soap and water and minimizing touching your face are suggested ways to reduce the likelihood of contracting the Novel Coronavirus COVID-19 infection.

    Centers for Disease Control: https://www.cdc.gov/coronavirus/2019-ncov/prepare/transmission.html

    The use of round doorknobs require a hand to grasp and twist to open a door. Entry and exit doors in particular must be touched by everyone entering a household, are usually touched before having access to a sink, and after potentially touching public handles, railings, shopping carts, elevator buttons, ATM’s, money, gas pumps, rest rooms… Doors with handles can be opened with elbows and forearms which are less likely to relay infection.

    This handle is easy to print and clamps firmly, gently, and non-destructively to round doorknobs.

    An added bonus: Handles are easier to open while carrying packages, groceries, or beverages!


    There are two styles, flat (push) and curved (pull).

    The flat version will likely print more easily and is perfectly fine for doors that push open.

    The curved version is tailored for doors that need to be pulled and in general is more universal.

    I can open our doors inside and out with my elbows, but even if you can’t get your elbow in there, at least you can limit exposure to one finger rather your whole hand grabbing a doorknob.

    You can customize the length, style, size of the doorknob and more in the OpenSCAD parameters in the file (even more if you dig into the code – have fun!).


    The default size is 54mm diameter, 23mm thick doorknob. The model can accommodate slightly larger and smaller sizes as is.

    To fit your doorknob you may either scale the STL file before printing (measure your doorknob diameter D and scale by D/54), or adjust the parameters in OpenSCAD, render (F6), save STL, and print your customized model.


    The door handle may be installed horizontally but I like ours elevated (rotated “up”) 30-50 degrees to provide more of a usable rotation arc with my elbow.

    Attach to doorknob

    1. Slip handle over your doorknob, it should fit snug but not crack
    2. Fasten with a machine screw or bolt (M3, 4-40, 2-56, or anything that will fit)
      • Lock nuts are preferred to regular nuts
      • Use of washers on both sides is recommended to distribute forces
    3. If snug enough, it may work fine without a screw
    4. Optional: If your handle slips, you can slip or wrap a rubber band between the doorknob and the ring before tightening the screw. You do not need to put the rubber band around the doorknob (that would be very difficult), it just needs to compress the rubber against the doorknob in one location to improve grip. Alternately, (more easily) you could wrap a rubber band around one arm of the handle ring prior to installation as pictured, but the rubber band will be visible once installed.

    Note, there should be enough friction to open the door with your elbow or knee
    but the plastic handle may not be able to grab the smooth metal doorknob with enough force to secure it against any force without slipping. Simply slide it back to the original position or you can try installing a rubber band for more “gription“.


    I will never know, but I hope this is helpful. I hope this has a chance of preventing at least one person from getting sick – even if it is not against Coronavirus, perhaps against one of the 200 viruses known to cause the common cold, or influenza (8,200 deaths in US in 2019), or Norovirus…

    Maybe you just want to make it easier to bring in your groceries.

    If you would like to support invention for the greater good, you can donate to your local makerspace… or mine: http://blog.workshop88.com/
    (they will need support during these times of social distancing and isolation)

    Also, please don’t judge my filthy garage/workshop doors too harshly… I have to go scrub them now.

    Stay well, wash your hands, and don’t touch your face!

    D. Scott Williamson
    Compulsively Creative