Reputation: 659
I am working on an iOS version of Game Of Life. The core of the app is working in that it is a working cellular automata with all the rules of game of life. It can make gliders, spaceships and function perfectly as such. If you don't know of Game of Life, youtube it.
So, what I am trying to do with this version is "smooth" out each cell. So instead of each cell being an isolated grid, all alive cells will be drawn together as a continuous blob.
I have gotten this to work already in a discrete form. What I mean by that is I have a set of rules for each cell where that the cell draws a selected curve/rectangular shape such that all the cells fit together.
Which looks like:
For example, the top cell on the right blob has one alive cell below it and know others so it gets 3 straight sides and a curved top. The rules to determine the shape of the cell are logic which is in the "cell" class. This produces the desired geometric effect I want. However, because there is a different CGPath for each cell, it is not animatable. Ideally, in this picture there would be two CGPaths.
Therein lies the problem. I'm now working on a way to compose vector path's based on the cell's alive in a given generation. This process will involve, I believe, having the cell's output coordinate points which are then taken in by some process to create paths. However the order those points are taken in will be important.
For example:
In this, 1) represents some set of "alive" "cells". 2) is a how I would generate the points. Basically using the same logic i use to create the curved paths from the first technique. It may have to be altered slightly as some points are not needed as they overlap cells. 3) would be the ideal vector shapes. Their are two vector paths because on cell has no neighbors.
So the algorithm I'm trying to think of is how to iterate over these points to create discrete path's for all sets of cell's mutually adjacent to each other. I'm looking into scanning left to right vs. top to bottom. Or always picking the point that is closest to the current point while still being part of the set of alive cell's. When it reaches the first point it will jump to the nearest point that isn't in that set of alive cells. Just theorizes at this point.
Upvotes: 3
Views: 183
Reputation: 29126
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;
}
Upvotes: 3