I'd been meaning to do this for awhile... when I was a kid, I was obsessed with a small program on the Apple II that had been printed in the magazine of the Puget Sound apple users group (I am really dating myself here). It did a two-dimensional bubble sort of colored blocks, ending up with these curved and tapered bands of color.
So I hacked it up in SuperCollider... basically just like I remember it

With 3600 blocks it will take quite some time to complete, but it's fascinating to watch as the random matrix slowly organizes itself from the outside in. A smaller matrix runs faster of course, but the bands are chunkier and not as pretty. If you get bored with it, just close the window and the routine will stop.
James
(
var blockSize = 10,
blocks = 60,
pixels = blockSize * blocks,
blockRect = Rect(0, 0, blockSize, blockSize),
brightness = { |color| color.red * color.green * color.blue },
colors = Array.fill(16, { Color.rand }).sort({ |a, b| brightness.(a) < brightness.(b) }),
matrix = Array.fill(blocks, { Array.fill(blocks, { colors.size.rand }) }),
sbounds = GUI.window.screenBounds,
w = GUI.window.new("HexaDecaColorDoubleBubbleSort",
Rect((sbounds.width - pixels) * 0.5, (sbounds.height - pixels * 0.5), pixels, pixels)),
views = matrix.collect({ |row, i|
row.collect({ |cell, j|
GUI.userView.new(w, Rect(i * blockSize, j * blockSize, blockSize, blockSize))
.relativeOrigin_(true)
.canFocus_(false)
.drawFunc_({ |view|
GUI.pen.color_(colors[matrix[i][j]])
.fillRect(blockRect)
});
});
}),
routine;
w.front;
routine = Routine({
var numSwaps = 1, temp,
// a bit tricky: I want to randomize whether it swaps
// horizontally first, or vertically
// so I put h-swap and v-swap functions in an array
// then, on each iteration, choose randomly which one to do first
swaps = [{ |i, j, k|
// horizontal swap
if((matrix[i][j] > matrix[i][k])) {
matrix[i].swap(j, k);
views[i][j].refresh;
views[i][k].refresh;
numSwaps = numSwaps + 1;
};
}, { |i, j, k|
// vertical swap
if(matrix[j][i] > matrix[k][i]) {
temp = matrix[j][i];
matrix[j][i] = matrix[k][i];
matrix[k][i] = temp;
views[j][i].refresh;
views[k][i].refresh;
numSwaps = numSwaps + 1;
};
}],
// another trick: going forward always causes a bias toward the bottom right
// so, we will scan forward first, then backward
fwdBackwd = Pseq(#[do, reverseDo], inf).asStream,
scanDirection;
while { numSwaps.debug("number of swaps last pass") > 0 } {
numSwaps = 0;
scanDirection = fwdBackwd.next;
blocks.perform(scanDirection, { |i|
(blocks-1).perform(scanDirection, { |j|
swaps[#[[0, 1], [1, 0]].choose].do({ |func|
func.value(i, j, j + 1);
});
0.01.wait;
});
});
};
"done".postln;
w.front; // no matter what I'm doing, I want this to pop up
}).play(AppClock);
w.onClose = { routine.stop };
)