For each live cell, create a bit mask of adjacent live cells. Say you use the bits with values 1, 2, 4, and 8 for north, east, south and west. You get a set of 16 tiles:

If you just want to display the cells with a rounded outline, you can quickly copy the appropriate tile to your canvas or bitmap window. That should be reasonably fast, because the tiles are already rendered.
If you really need the outline as a curve, create the dark red lines as curves for each tile. Note that the tiles 0101
and 1010
consist of two curves, the tile 1111
doesn't have a curve and the tile 0000
is a closed curve.
Move the curve segments with an offset for the current cell to a list. All curves (except 0000
) have loose ends; these ends always lie on cell border intersections.
Now you can create paths. Create a look-up table, maybe a hash, of all end points. Each end point should have two connected curve segments. Now pick a curve segment from the list. Its starting point is the starting point of the curve. Now look up the segment's end point and go to the next segment. Remove visited segments and add them to a path. Continue until you reach the
starting point of the curve. Add the curve to a list. Repeat while there are still curve segments in the list.
You should probably put cells without neighbours (0000
) into the final curve list right away. You must also treat the two line segments of the 0101
and 1010
cells as separate segments.
Edit: I've cobbled together a proof-of-concept example in C that takes a randomly generated cell grid and creates a PostScript filem of the smooth rendering.
The nomenclature is a bit inconsitent, I'm afraid, but there are basically four segment types: closed circles, spikes (or noses), quarter-circles and straight walls. These are rendered in clockwise fashion, so that the enclosed shape is always to the right of the curve.
The PS rendering – the path
field of stype
– is made up of quarter-circles (NE
, SE
, SW
, NW
), of straight lines of the cell size (N1
, E1
, S1
, W1
) and of straight lines of half the cell size (N2
, E2
, S2
, W2
). Instead of using PS commands, you can render them as sequences of straight lines and cubic Bézier curves, of course.
Each segment also stores where the start and end corner of a cell is with the NE
, SE
, SW
and NW
enumeration.
In addition to the cells, there is a list of segments and a grid of corners, which stores which segments are attached to a corner. A corner can only have no or two segments attached.
The example has a size that is fixed a compile time to make life for the C programmer easier. Here goes:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define N 24
#define die(...) exit((fprintf(stderr, __VA_ARGS__), 1))
enum {NW, NE, SE, SW, NCORNER};
static const int dx[NCORNER] = {0, 1, 1, 0};
static const int dy[NCORNER] = {0, 0, 1, 1};
struct stype {
const char *name;
int start;
int end;
const char *path;
};
#define STYPE(X) \
X(None, -1, -1, "") \
X(WallN, NW, NE, "E1") \
X(WallE, NE, SE, "S1") \
X(WallS, SE, SW, "W1") \
X(WallW, SW, NW, "N1") \
X(QuarterNE, SE, NW, "W2 SW N2") \
X(QuarterSE, SW, NE, "N2 NW E2") \
X(QuarterSW, NW, SE, "E2 NE S2") \
X(QuarterNW, NE, SW, "S2 SE W2") \
X(SpikeN, NE, NW, "S2 SE SW N2") \
X(SpikeE, SE, NE, "W2 SW NW E2") \
X(SpikeS, SW, SE, "N2 NW NE S2") \
X(SpikeW, NW, SW, "E2 NE SE W2") \
X(Circle, NW, NW, "MM")
#define STYPE_STRUCT(Name, S1, S2, P) {#Name, S1, S2, P},
#define STYPE_ENUM(Name, S1, S2, P) Name,
enum {STYPE(STYPE_ENUM) MAX_STYPE};
static const struct stype stype[MAX_STYPE] = { STYPE(STYPE_STRUCT)};
int ctype[16][3] = {
/* 0 */ {Circle, None},
/* 1 */ {SpikeN, None},
/* 2 */ {SpikeE, None},
/* 3 */ {QuarterNE, None},
/* 4 */ {SpikeS, None},
/* 5 */ {WallW, WallE, None},
/* 6 */ {QuarterSE, None},
/* 7 */ {WallW, None},
/* 8 */ {SpikeW, None},
/* 9 */ {QuarterNW, None},
/* 10 */ {WallN, WallS, None},
/* 11 */ {WallS, None},
/* 12 */ {QuarterSW, None},
/* 13 */ {WallE, None},
/* 14 */ {WallN, None},
/* 15 */ { None},
};
struct seg {
int type;
int x;
int y;
};
struct pt {
int seg1;
int seg2;
};
int alive(int cell[N][N], int x, int y)
{
if (x < 0 || x >= N) return 0;
if (y < 0 || y >= N) return 0;
return cell[y][x];
}
void addpt(struct pt *pt, int seg)
{
if (pt->seg1 < 0) {
pt->seg1 = seg;
} else if (pt->seg2 < 0) {
pt->seg2 = seg;
} else {
die("Too many segments for point.\n");
}
}
int main(void)
{
int cell[N][N];
int count = 0;
int i, x, y;
for (y = 0; y < N; y++) {
for (x = 0; x < N; x++) {
int r = 1 + abs(N/2 - x) + abs(N/2 - y);
cell[y][x] = (rand() / 4 < RAND_MAX / r);
if (cell[y][x]) count++;
}
}
/* Create line segments */
struct seg seg[2 * count];
int nseg = 0;
for (y = 0; y < N; y++) {
for (x = 0; x < N; x++) {
int ix = 0;
if (cell[y][x] == 0) continue;
if (alive(cell, x, y - 1)) ix |= 1;
if (alive(cell, x + 1, y)) ix |= 2;
if (alive(cell, x, y + 1)) ix |= 4;
if (alive(cell, x - 1, y)) ix |= 8;
int *p = ctype[ix];
while (*p != None) {
if (nseg >= 2 * count) die("Segment overflow\n");
seg[nseg].x = x;
seg[nseg].y = y;
seg[nseg].type = *p++;
nseg++;
}
}
}
/* determine start and end points of segments */
struct pt pt[N + 1][N + 1];
memset(pt, -1, sizeof(pt));
for (i = 0; i < nseg; i++) {
int tp = seg[i].type;
int s = stype[tp].start;
int e = stype[tp].end;
x = seg[i].x;
y = seg[i].y;
addpt(&pt[y + dy[s]][x + dx[s]], i);
addpt(&pt[y + dy[e]][x + dx[e]], i);
}
/* set up PostScript header */
puts("%!PS-Adobe 3.0");
puts("/A 10 def");
puts("/A2 A 2 mul def");
puts("/C { rcurveto } def");
puts("/L { rlineto } def");
puts("/START { newpath exch A2 mul exch A2 mul moveto } bind def");
puts("/END { closepath stroke } bind def");
puts("/MM { A 0 rmoveto NE SE SW NW } bind def");
puts("/NW { 0 A neg 0 A neg A A neg C } bind def");
puts("/NE { A 0 A 0 A A C } bind def");
puts("/SE { 0 A 0 A A neg A C } bind def");
puts("/SW { A neg 0 A neg 0 A neg A neg C } bind def");
puts("/N1 { 0 A2 neg L } bind def");
puts("/E1 { A2 0 L } bind def");
puts("/S1 { 0 A2 L } bind def");
puts("/W1 { A2 neg 0 L } bind def");
puts("/N2 { 0 A neg L } bind def");
puts("/E2 { A 0 L } bind def");
puts("/S2 { 0 A L } bind def");
puts("/W2 { A neg 0 L } bind def");
puts("57 180 translate");
/* walk segments */
for (i = 0; i < nseg; i++) {
struct seg *s = seg + i;
if (s->type == None) continue;
int x0 = s->x + dx[stype[s->type].start];
int y0 = s->y + dy[stype[s->type].start];
int j = i;
x = s->x + dx[stype[s->type].end];
y = s->y + dy[stype[s->type].end];
printf("%d %d START", x0, y0);
for (;;) {
printf(" %s", stype[s->type].path);
s->type = None;
if (x == x0 && y == y0) break;
if (pt[y][x].seg1 == j) {
j = pt[y][x].seg2;
} else {
j = pt[y][x].seg1;
}
s = seg + j;
x = s->x + dx[stype[s->type].end];
y = s->y + dy[stype[s->type].end];
}
puts(" END");
}
puts("showpage");
return 0;
}