from __future__ import division import golly as g from math import floor, ceil, log from os import system, unlink resx, resy = 1920, 1080 # woo 1080p osa = 3 # 3x3 oversampling framerate = 30 #resx, resy = 160, 90 #osa = 2 #framerate = 5 framecount = framerate*90 # 90 seconds long seems reasonable outputmask = "/home/phlip/lifeanim/%08d.%s" # outputmask % (frameno, extension) cellsize = osa # make cells larger by a full pixel in each direction, to cut down on moire effects centrex, centrey = 114500, 4365 initw, inith = 96, 54 finalscale = 11.0 # so that we scale out by a factor of 2**11 over the animation finalratefact = 17.0 # so that we speed up by a factor of 2**17 over the animation (the metapixels animate at about 2**-15 of the speed) initrate = 1 # initially approx one step per second initrateadj = initrate * framecount/framerate/(finalratefact*log(2)) # magic calculation to make it so that d/dt of setframe(t) at 0 is initrate curstep = 0 def setstep(n): global curstep n = int(round(n)) if n < curstep: return g.run(n - curstep) curstep = n def setframe(t): setstep(initrateadj * 2**(finalratefact * t)) class Rect: def __init__(self, l, t, w, h): self.left = l self.top = t self.width = w self.height = h def calcscale(t): if t < 3*framerate/framecount: t = 3*framerate/framecount # don't zoom out for the first 3 seconds w = initw * 2**(finalscale * t) h = inith * 2**(finalscale * t) l = centrex - w / 2.0 t = centrey - h / 2.0 return Rect(l,t,w,h) def clamp(x,minim,maxim): if x < minim: return minim if x > maxim: return maxim return x def doframe(n): t = n/framecount setframe(t) sc = calcscale(t) # get all the cells in the view area cells = g.getcells([sc.left, sc.top, sc.width, sc.height]) # generate a large image (for oversampling) output = [[0 for x in xrange(resx * osa)] for y in xrange(resy * osa)] # draw all the live cells on the image for i in xrange(0, len(cells), 2): cellx, celly = cells[i:i+2] # find the bounds of this cell on the screen xmin = int(floor(((cellx - sc.left) / sc.width) * resx * osa)) xmax = int(floor(((cellx - sc.left + 1) / sc.width) * resx * osa)) + 1 ymin = int(floor(((celly - sc.top) / sc.height) * resy * osa)) ymax = int(floor(((celly - sc.top + 1) / sc.height) * resy * osa)) + 1 # make the cells slightly larger and overlapping, to reduce the moire effects xmin -= cellsize ymin -= cellsize xmax += cellsize ymax += cellsize # draw the cell on the image xmin = clamp(xmin, 0, resx * osa) xmax = clamp(xmax, 0, resx * osa) ymin = clamp(ymin, 0, resy * osa) ymax = clamp(ymax, 0, resy * osa) for y in xrange(ymin, ymax): output[y][xmin:xmax] = [1] * (xmax - xmin) # Scale down the oversampled image to the target size, by averaging if osa > 1: output = [[sum(output[i][j] for i in xrange(y*osa,(y+1)*osa) for j in xrange(x*osa,(x+1)*osa)) for x in xrange(resx)] for y in xrange(resy)] # Save the image as a PGM, and then use ImageMagick to convert it to PNG with open(outputmask % (n, "pgm"), 'w') as fp: fp.write("P2\n%d %d\n%d\n" % (resx, resy, osa*osa)) sc = calcscale(t) for row in output: fp.write(' '.join(map(str,row))) fp.write('\n') system("convert %s -depth 8 %s" % (outputmask % (n, "pgm"), outputmask % (n, "png"))) unlink(outputmask % (n, "pgm")) # Parameters so that I can run several instances of Golly and have them render separate slices of the frames # and thus make use of my multicore CPU... def main(step=1,start=0): global curstep g.reset() curstep = 0 for i in xrange(start,framecount,step): g.show("Frame %d of %d..." % (i+1, framecount)); doframe(i) main() # or, make copies of the script and have each copy have one of, eg: #main(3,0) #main(3,1) #main(3,2) # and then run three instances of Golly and have each run a different copy of the script