I saw an icon of an animated twisting of two strands. While it was interesting and reasonably well done there were some things that I saw that didn't quite fit. The thing that bugged me the most was that the image was not perfectly looping - there was a gap of a few pixels in the image where the two strands were bound together, some white space, and then two new strands were twisted. To me, it would have been better if the image just kept going on and on without that break.

So, I decided to sit down and do my own looping strands. The most interesting is that of the braid with three strands. Opening up gimp and trying to do one frame, it quickly became apparent that this was a job for a program.

In writing the program, the first step was to write to an image format that was not a pain to write to. For this, PPM is perfectly suited. The current GD library cannot write to a .gif (the key to animated images) and I'm not 100% certain I'd want to use GD to write to it - it was much easier to write to the PPM file.

The second step is to get three strands to appear on the screen. These are sin waves that are separated by 120 degrees (or 2π/3 and 4π/3 in radians). At each spot, a 3x3 block was drawn to cover up the holes between images and make the line thick enough to see.

The $64,000 question is what strand goes over what strand? Classically, we recite this as "outside over center" or "left over center, right over center, left over center, right over center...". This doesn't translate into code well... or does it? Looking at the diagram of a braid that I had drawn out, and looking at the specific instances of the strands crossing a pattern rapidly became apparent:

  • If the crossing is in the lower half, the strand with the positive slope is on top.
  • If the crossing is in the upper half, the strand with the negative slope is on top.
Fortunately, the question of slope was trivial - the derivative of sin(x) is cos(x) (lucky me).

Once you have the PPM files, a program such as ppmtogif is then used to translate the images from ppm format to gif format, at which point your favorite gif animator can be used.

In sort:

  • Loop over the number of frames
    • Determine file name (0 padded so that UNIX globs sort correctly)
    • Open the file for write
    • Print the header of the PPM file
    • initialize the image array
    • Loop over x for the frame
      • Assign 3x3 block for red (0 offset), green (2π/3 offset), blue (4π/3 offset) for each x at y = 16 + 16(sin(x + offset + frame offset)
      • store the cos for each color (offset) for use in overlap
    • Loop over y, x
      • For each color pair (RG, RB, BG), if there is an overlap, look at the value of y and the cos to determine which should be on top.
      • Print the RGB value to the ppm file
      • Close the file

The perl code follows.

#!/usr/bin/perl

use strict;
my $max = 40;
my $pi = 22/7;

for(my $k = 0; $k < $max; $k++) {
    my $zk = sprintf("%0.2d",$k);
    open (FILE,">ppm/$zk");

    print FILE << "PPMHEAD";
P3
32 32
1
PPMHEAD

    my @img;
    foreach my $x (0 .. 31) {
        foreach my $y (0 .. 31) {
            foreach my $c (0,1,2) {
                $img[$x][$y][$c][0] = 0;
                $img[$x][$y][$c][1] = 0;
            }
        }
    }

# $dx = period within frame
# $dk = period within anim
    foreach my $x (0 .. 31) {
        my $dx = $x / 8;
        my $dk = $k / (2*$pi);
        foreach my $deltax (-1,0,1) {
            foreach my $deltay (-1,0,1) {
                next if($x == 0 and $deltax == -1);
                $img[$x+$deltax][$deltay + 16 +
                (16 * sin($dx + (0*$pi/3) + $dk))][0][0] = 1;
                $img[$x+$deltax][$deltay + 16 +
                (16 * sin($dx + (0*$pi/3) + $dk))][0][1] = cos($dx + (0*$pi/3) + $dk);

                $img[$x+$deltax][$deltay + 16 +
                (16 * sin($dx + (2*$pi/3) + $dk))][1][0] = 1;
                $img[$x+$deltax][$deltay + 16 +
                (16 * sin($dx + (2*$pi/3) + $dk))][1][1] = cos($dx + (2*$pi/3) + $dk);

                $img[$x+$deltax][$deltay + 16 +
                (16 * sin($dx + (4*$pi/3) + $dk))][2][0] = 1;
                $img[$x+$deltax][$deltay + 16 +
                (16 * sin($dx + (4*$pi/3) + $dk))][2][1] = cos($dx + (4*$pi/3) + $dk);
            }
        }
    }

    foreach my $y (0 .. 31) {
        foreach my $x (0 .. 31) {
            if ($img[$x][$y][0][0] and $img[$x][$y][1][0]) {
                if($y < 16) { # in neg half
                    if($img[$x][$y][0][1] > 0)
                        { print FILE "1 0 0"; }
                    else
                        { print FILE "0 1 0"; }
                }
                else { # in pos half
                    if($img[$x][$y][0][1] < 0)
                        { print FILE "1 0 0"; }
                    else
                        { print FILE "0 1 0"; }
                }
            }
            elsif ($img[$x][$y][1][0] and $img[$x][$y][2][0]) {
                if($y < 16) {        # in neg half
                    if($img[$x][$y][1][1] > 0)
                        { print FILE "0 1 0"; }
                    else
                        { print FILE "0 0 1"; }
                }
                else {  # in pos half
                    if($img[$x][$y][0][1] < 0)
                        { print FILE "0 1 0"; }
                    else
                        { print FILE "0 0 1"; }
                }
            }
            elsif ($img[$x][$y][0][0] and $img[$x][$y][2][0]) {
                if($y < 16) {# in neg half
                    if($img[$x][$y][0][1] > 0)
                        { print FILE "1 0 0"; }
                    else
                        { print FILE "0 0 1"; }
                }
                else {  # in pos half
                    if($img[$x][$y][0][1] < 0)
                        { print FILE "1 0 0"; }
                    else
                        { print FILE "0 0 1"; }
                }
            }
            else {      # no overlap
                print FILE "$img[$x][$y][0][0] "; # R
                print FILE "$img[$x][$y][1][0] "; # G
                print FILE "$img[$x][$y][2][0] "; # B
            }
            print FILE "\n";
        }
    }
    close(FILE);
}