Created
June 1, 2017 00:43
-
-
Save dkilmer/90556fe6f6a8a26d6f89dbe87601870c to your computer and use it in GitHub Desktop.
Creates random 32x32 tiles that can be connected together to make a pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Generates 32x32 tiles that can be connected with conduits on one or | |
* more sides. The basic process is: | |
* | |
* 1. Draw a bunch of random boxes with color X, none of which touch the outer perimeter. | |
* 2. draw some conduits from the edges -- each conduit is two color-X lines going toward | |
* the center until they hit a color-X pixel | |
* 3. flood fill from the four corners of the perimeter with color Y. | |
* 4. erase all X-color pixels that don't have an adjacent Y-color pixel in the four | |
* cardinal directions. | |
* 5. fix the inside corner pixels that got erased in the previous step (see completeLines() | |
* for the logic of what's an inside corner). | |
* 6. iteratively draw along the inside of the shape, using a different pixel color each | |
* time (see contractLines() for the logic of finding the inside of the shape). | |
* | |
* One thing lacking in this code is that drawing some random number of boxes does not | |
* guarantee a single contiguous shape. To make it dependable, I'd need to write a test | |
* to see if the shape is contiguous. A naive implementation of that would be to see if | |
* flood-filling from any corner caused another corner to be filled. That would be really | |
* inefficient, though. | |
*/ | |
int xoff = 20; | |
int yoff = 20; | |
int bwidth = 10; | |
int FRAME_INTERVAL = 30; | |
int IMG_WIDTH = 672; | |
int IMG_HEIGHT = 720; | |
int ROW_PIX = 48 * IMG_WIDTH; | |
int COL_PIX = 42; | |
int X_OFFSET = 8; | |
int Y_OFFSET = 14; | |
int[] tiles = new int[1024]; | |
int dt; | |
int time; | |
int frames; | |
int evt_idx = 0; | |
int lc_idx = 0; | |
int conds = 1; | |
int cond_cnt = 0; | |
int tile_idx = 0; | |
ArrayList<Ev> events = new ArrayList<Ev>(); | |
int FILL_COLOR = #E0E0E0; | |
int MARK_COLOR = #0000FF; | |
int NONE_COLOR = 0; | |
int[] LINE_COLOR = new int[4]; | |
// directions - defined to allow bit-masking | |
int LEFT = 1; | |
int UP = 2; | |
int RIGHT = 4; | |
int DOWN = 8; | |
// events | |
int E_DRAW_BOX = 0; | |
int E_FLOOD_FILL = 1; | |
int E_CULL = 2; | |
int E_CONDUIT = 3; | |
int E_CONTRACT_LINES = 4; | |
int E_WAIT = 5; | |
int E_RESET = 6; | |
void setup() { | |
size(360, 360); | |
randomSeed(142857); | |
clear(); | |
frames = 0; | |
LINE_COLOR[0] = #4a7423; | |
LINE_COLOR[1] = #588b29; | |
LINE_COLOR[2] = #639c2e; | |
LINE_COLOR[3] = #72b434; | |
// create a list of events to perform repeatedly | |
events.add(new Ev(E_DRAW_BOX, rndInt(7, 16), 5)); | |
events.add(new Ev(E_CONDUIT, 1, 10)); | |
events.add(new Ev(E_FLOOD_FILL, 1, 10)); | |
events.add(new Ev(E_CULL, 1, 10)); | |
events.add(new Ev(E_CONTRACT_LINES, 20, 10)); | |
events.add(new Ev(E_WAIT, 4, 10)); | |
resetEvents(); | |
time = millis(); | |
makeAll(); | |
} | |
// clear the 32x32 array of tiles | |
void clear() { | |
for (int i=0; i<1024; i++) { | |
tiles[i] = NONE_COLOR; | |
} | |
} | |
// after all the events have been processed, reset so we can do it again. | |
void resetEvents() { | |
evt_idx = 0; | |
lc_idx = 0; | |
for (int i=0; i<events.size(); i++) { | |
Ev evt = events.get(i); | |
evt.cnt = 0; | |
if (evt.etype == E_DRAW_BOX) { | |
evt.num_times = rndInt(7, 16); | |
} | |
} | |
FRAME_INTERVAL = events.get(0).frame_dur; | |
} | |
// remove a single color from the array of tiles | |
void clearColor(int rgb) { | |
for (int i=0; i<1024; i++) { | |
if (tiles[i] == rgb) tiles[i] = NONE_COLOR; | |
} | |
} | |
// replace one color with another in the array of tiles | |
void replaceColor(int orgb, int nrgb) { | |
for (int i=0; i<1024; i++) { | |
if (tiles[i] == orgb) tiles[i] = nrgb; | |
} | |
} | |
// draw criss-crossy lines so we can see where the tiles are. | |
void drawGrid() { | |
noFill(); | |
stroke(150); | |
for (int y=0; y<32; y++) { | |
for (int x=0; x<32; x++) { | |
int xx = xoff+(x*bwidth); | |
int yy = yoff+(y*bwidth); | |
rect(xx, yy, bwidth, bwidth); | |
} | |
} | |
} | |
// draw a single tile in the desired color | |
void drawTile(int x, int y, int rgb) { | |
int xx = xoff+(x*bwidth)+1; | |
int yy = yoff+(y*bwidth)+1; | |
stroke(rgb); | |
fill(rgb); | |
rect(xx, yy, bwidth-2, bwidth-2); | |
} | |
// draw all the tiles in the array | |
void drawTiles() { | |
for (int y=0; y<32; y++) { | |
for (int x=0; x<32; x++) { | |
int idx = (y*32)+x; | |
if (tiles[idx] != NONE_COLOR) { | |
drawTile(x, y, tiles[idx]); | |
} | |
} | |
} | |
} | |
// probably a dumb implementation | |
int rndInt(int min, int max) { | |
return round(random(min, max)); | |
} | |
// set a tile in the array to a particular color | |
void setTile(int x, int y, int rgb) { | |
tiles[(y*32)+x] = rgb; | |
} | |
// get the color of a tile in the array, returning -1 if off the edge. | |
int getTile(int x, int y) { | |
if (x<0 || x>31) return -1; | |
if (y<0 || y>31) return -1; | |
return tiles[(y*32)+x]; | |
} | |
// put a random box in the tile array, avoiding the outer edge | |
void randomBox() { | |
int x1 = rndInt(3, 24); | |
int y1 = rndInt(3, 24); | |
int x2 = rndInt(x1+3, 29); | |
int y2 = rndInt(y1+3, 29); | |
for (int x=x1; x<=x2; x++) { | |
setTile(x, y1, LINE_COLOR[0]); | |
setTile(x, y2, LINE_COLOR[0]); | |
} | |
for (int y=y1+1; y<y2; y++) { | |
setTile(x1, y, LINE_COLOR[0]); | |
setTile(x2, y, LINE_COLOR[0]); | |
} | |
} | |
// create a conduit in the tile array from a particular direction | |
void conduit(int dir) { | |
int x1, y1, x2, y2, inc; | |
if (dir == UP || dir == DOWN) { | |
x1 = 11; | |
x2 = 20; | |
if (dir == UP) { | |
y1 = 0; | |
inc = 1; | |
} else { | |
y1 = 31; | |
inc = -1; | |
} | |
int y = y1; | |
while (getTile(x1, y) == NONE_COLOR) { | |
//while (y != 16) { | |
setTile(x1, y, LINE_COLOR[0]); | |
y += inc; | |
} | |
y = y1; | |
while (getTile(x2, y) == NONE_COLOR) { | |
//while (y != 16) { | |
setTile(x2, y, LINE_COLOR[0]); | |
y += inc; | |
} | |
} else { | |
y1 = 11; | |
y2 = 20; | |
if (dir == LEFT) { | |
x1 = 0; | |
inc = 1; | |
} else { | |
x1 = 31; | |
inc = -1; | |
} | |
int x = x1; | |
while (getTile(x, y1) == NONE_COLOR) { | |
//while (x != 16) { | |
setTile(x, y1, LINE_COLOR[0]); | |
x += inc; | |
} | |
x = x1; | |
while (getTile(x, y2) == NONE_COLOR) { | |
//while (x != 16) { | |
setTile(x, y2, LINE_COLOR[0]); | |
x += inc; | |
} | |
} | |
} | |
// flood fill the tile array from point (x,y) with color crgb, stopping at color brgb | |
void floodFill(int x, int y, int brgb, int crgb) { | |
int rgb = getTile(x, y); | |
if (rgb != brgb) return; | |
setTile(x, y, crgb); | |
floodFill(x-1, y, brgb, crgb); | |
floodFill(x+1, y, brgb, crgb); | |
floodFill(x, y-1, brgb, crgb); | |
floodFill(x, y+1, brgb, crgb); | |
} | |
// check whether a tile in the array should be an inside corner of a line | |
boolean isInsideCorner(int x, int y, int crgb) { | |
if (getTile(x-1, y) == crgb && getTile(x, y-1) == crgb && getTile(x-1, y-1) != crgb) { | |
return true; | |
} | |
if (getTile(x+1, y) == crgb && getTile(x, y-1) == crgb && getTile(x+1, y-1) != crgb) { | |
return true; | |
} | |
if (getTile(x-1, y) == crgb && getTile(x, y+1) == crgb && getTile(x-1, y+1) != crgb) { | |
return true; | |
} | |
if (getTile(x+1, y) == crgb && getTile(x, y+1) == crgb && getTile(x+1, y+1) != crgb) { | |
return true; | |
} | |
return false; | |
} | |
// remove all tiles of color crgb that aren't next to a tile of color nrgb | |
void cull(int crgb, int nrgb) { | |
for (int y=0; y<32; y++) { | |
for (int x=0; x<32; x++) { | |
int rgb = getTile(x, y); | |
if (rgb == crgb) { | |
if (getTile(x-1, y) != nrgb && getTile(x+1, y) != nrgb && getTile(x, y-1) != nrgb && getTile(x, y+1) != nrgb) { | |
setTile(x, y, NONE_COLOR); | |
} | |
} | |
} | |
} | |
} | |
// fill in all the inside corners | |
void completeLines(int crgb, int nrgb) { | |
for (int y=0; y<32; y++) { | |
for (int x=0; x<32; x++) { | |
int rgb = getTile(x, y); | |
if (rgb == NONE_COLOR && isInsideCorner(x, y, crgb)) { | |
setTile(x, y, MARK_COLOR); | |
} | |
} | |
} | |
replaceColor(MARK_COLOR, crgb); | |
} | |
// for a tile along the perimeter of the shape, determine what direction | |
// points to the inside of the shape. This assumes the outside of the shape | |
// is filled with some other color while the inside is all set to NONE_COLOR. | |
int getDirOfInsideTile(int x, int y) { | |
if (getTile(x-1, y) == NONE_COLOR) return LEFT; | |
if (getTile(x+1, y) == NONE_COLOR) return RIGHT; | |
if (getTile(x, y-1) == NONE_COLOR) return UP; | |
if (getTile(x, y+1) == NONE_COLOR) return DOWN; | |
return -1; | |
} | |
// draw lines along the inside of a shape, where the edge to draw along | |
// is color crgb and the new color to draw is nrgb | |
boolean contractLines(int crgb, int nrgb) { | |
boolean contracted = false; | |
for (int y=0; y<32; y++) { | |
for (int x=0; x<32; x++) { | |
int rgb = getTile(x, y); | |
if (rgb == crgb) { | |
int dir = getDirOfInsideTile(x, y); | |
while (dir >=0) { | |
if (dir == LEFT) { | |
setTile(x-1, y, nrgb); | |
contracted = true; | |
} else if (dir == UP) { | |
setTile(x, y-1, nrgb); | |
contracted = true; | |
} else if (dir == RIGHT) { | |
setTile(x+1, y, nrgb); | |
contracted = true; | |
} else if (dir == DOWN) { | |
setTile(x, y+1, nrgb); | |
contracted = true; | |
} | |
dir = getDirOfInsideTile(x, y); | |
} | |
} | |
} | |
} | |
return contracted; | |
} | |
// save the array to an actual image for saving to file | |
void saveTileImage(PImage img) { | |
int tx = tile_idx % 16; | |
int ty = tile_idx / 16; | |
for (int y=0; y<32; y++) { | |
for (int x=0; x<32; x++) { | |
int rgb = getTile(x, y); | |
int off = ((ty * ROW_PIX) + (Y_OFFSET * IMG_WIDTH) + (y * IMG_WIDTH)) + ((tx * COL_PIX) + X_OFFSET + x); | |
if (rgb == NONE_COLOR || rgb == FILL_COLOR) { | |
img.pixels[off] = color(0,0,0,0); | |
} else { | |
int r = (rgb & 0xFF0000) >> 16; | |
int g = (rgb & 0xFF00) >> 8; | |
int b = (rgb & 0xFF); | |
img.pixels[off] = color(r,g,b,255); | |
} | |
} | |
} | |
} | |
// this just does everything at once, eschewing the whole drawing-to-the-screen | |
// thing. it creates 16 random shapes for each of the 15 possible combinations | |
// of conduits and puts them all into an image that has 15 rows of 16 32x32 shapes. | |
void makeAll() { | |
PImage img = createImage(IMG_WIDTH,IMG_HEIGHT, ARGB); | |
img.loadPixels(); | |
int dir = 1; | |
int dcnt = 0; | |
for (tile_idx=0; tile_idx<240; tile_idx++) { | |
// draw random boxes | |
int num_boxes = rndInt(7, 16); | |
for (int i=0; i<num_boxes; i++) { | |
randomBox(); | |
} | |
// draw conduits | |
if ((dir & LEFT) == LEFT) conduit(LEFT); | |
if ((dir & UP) == UP) conduit(UP); | |
if ((dir & RIGHT) == RIGHT) conduit(RIGHT); | |
if ((dir & DOWN) == DOWN) conduit(DOWN); | |
// flood fill the outside | |
floodFill(0, 0, 0, FILL_COLOR); | |
floodFill(31, 0, 0, FILL_COLOR); | |
floodFill(0, 31, 0, FILL_COLOR); | |
floodFill(31, 31, 0, FILL_COLOR); | |
// cull the inside and complete the lines | |
cull(LINE_COLOR[0], FILL_COLOR); | |
completeLines(LINE_COLOR[0], FILL_COLOR); | |
// contract and draw inner lines | |
boolean contracted = true; | |
int clr_idx = 0; | |
while (contracted) { | |
int crgb = LINE_COLOR[clr_idx]; | |
clr_idx++; | |
if (clr_idx > 3) clr_idx = 0; | |
int nrgb = LINE_COLOR[clr_idx]; | |
contracted = contractLines(crgb, nrgb); | |
completeLines(nrgb, FILL_COLOR); | |
} | |
saveTileImage(img); | |
clear(); | |
dcnt++; | |
if (dcnt > 15) { | |
dcnt = 0; | |
dir++; | |
} | |
} | |
img.updatePixels(); | |
img.save("mosses.png"); | |
} | |
// execute the current event in the queue and advance to the next event | |
void execEvent() { | |
Ev evt = events.get(evt_idx); | |
FRAME_INTERVAL = evt.frame_dur; | |
if (evt.etype == E_DRAW_BOX) { | |
randomBox(); | |
} else if (evt.etype == E_CONDUIT) { | |
int dir = rndInt(1, 15); | |
if ((dir & LEFT) == LEFT) conduit(LEFT); | |
if ((dir & UP) == UP) conduit(UP); | |
if ((dir & RIGHT) == RIGHT) conduit(RIGHT); | |
if ((dir & DOWN) == DOWN) conduit(DOWN); | |
} else if (evt.etype == E_FLOOD_FILL) { | |
floodFill(0, 0, 0, FILL_COLOR); | |
floodFill(31, 0, 0, FILL_COLOR); | |
floodFill(0, 31, 0, FILL_COLOR); | |
floodFill(31, 31, 0, FILL_COLOR); | |
} else if (evt.etype == E_CULL) { | |
cull(LINE_COLOR[0], FILL_COLOR); | |
completeLines(LINE_COLOR[0], FILL_COLOR); | |
} else if (evt.etype == E_CONTRACT_LINES) { | |
int crgb = LINE_COLOR[lc_idx]; | |
lc_idx++; | |
if (lc_idx > 3) lc_idx = 0; | |
int nrgb = LINE_COLOR[lc_idx]; | |
boolean contracted = contractLines(crgb, nrgb); | |
completeLines(nrgb, FILL_COLOR); | |
if (!contracted) evt.cnt = evt.num_times; | |
} else if (evt.etype == E_WAIT) { | |
} | |
evt.inc(); | |
if (evt.done()) evt_idx++; | |
if (evt_idx >= events.size()) { | |
resetEvents(); | |
clear(); | |
} | |
} | |
// draw a frame to the screen | |
void draw() { | |
int t = millis(); | |
dt = t - time; | |
time = t; | |
frames++; | |
if (frames == FRAME_INTERVAL) { | |
frames = 0; | |
execEvent(); | |
} | |
background(255); | |
drawGrid(); | |
drawTiles(); | |
} | |
// the event class. it has a type, a number of times to execute before | |
// moving to the next event, a count of how many times it has executed so | |
// far, and how many frames to wait after completing before starting on | |
// the next event. | |
class Ev { | |
int etype; | |
int num_times; | |
int cnt; | |
int frame_dur; | |
Ev(int t, int nt, int fd) { | |
etype = t; | |
num_times = nt; | |
frame_dur = fd; | |
clear(); | |
} | |
void clear() { | |
cnt = 0; | |
} | |
boolean inc() { | |
cnt++; | |
return (cnt == num_times); | |
} | |
boolean done() { | |
return (cnt >= num_times); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment