Youtube video here – NB: select 720p HD quality
Each time it’s run, the program will make frames for a different movie; with current settings, 5000 frames for a 15fps movie.

where
,
,
and
range between
and
. It’s identical to the basic Sierpinski triangle IFS except the 3 maps are randomly changing position (
), x and y shrink (
), and angle (
).
++++The maps are assigned the colours R, G, B. The pixel colour encodes the previous 8 maps that were chosen: The latest map is 128, the one before 64, the one before 32 etc. So that if e.g. the 8 last maps used were all the red one, the pixel will be coloured RGB=255,0,0. Up to 10 colours are stored for each pixel, which are averaged at the end to find the colour for the image.
If the point goes off screen, adjust variables and start again. Continue until either 1000 points have been calculated with nothing drawn (just revisiting points) or 10 million points plotted, then save image.
I made a 15 frames/sec, h264 video with an old version of ffmpeg, with crf 15. (crf 23 looked fine; crf 15 makes about a 50% larger file.)
Cython program IFSmoviev9.pyx:
On the Sage command line %attach IFSmoviev9.pyx then OK() to run it.
#cython: boundscheck=False, wraparound=False, cdivision=True, nonecheck=False
# IFS movie maker......v9 - june 2017
#ffmpeg command to make 15 fps movie from images:
#ffmpeg -f image2 -r 15 -i ~/ifs/0%04d.png -vcodec h264 -crf 23 ifsv9.mp4
import numpy as np
cimport numpy as np
from scipy import misc
from libc.stdlib cimport rand
from libc.math cimport sin,cos,fabs,sqrt
DEF EQNS = 2 #number of equations i.e. currently x and y -> 2 dimensions
DEF COEFFS = 2 #i.e. a, b (=coef[0],coef[1].)
DEF COLOURS = 3 #i.e. R,G,B currently
DEF SCREENWID = 1280
DEF SCREENHEI = 720
DEF PI = 3.14159265
DEF MAXLOOPS = 10000000
DEF MARGIN = 30 #20 #50
DEF REV = -0.2 #reverse: multiply speed by this when edge of screen/min/max reached
DEF MAXCONSTV = 0.5 #the most the C terms are allowed to change each time.
DEF MAXCONSTA = 0.1 #max translation acceleration , i.e. velocity difference
DEF SHRINKSTEP = 0.001 #amount largest shrink is reduced by if image is too big.
DEF REBOUNDRATIO = 0.005 # as if a car travelling 100m/s hit a wall and rebounds at 0.5m/s
DEF MAXSHRINKV = 0.05
DEF MAXSHRINKA = 0.01
DEF MAXANGV = 2*PI/(10*15) # at 15fps, max turn speed = 360 degrees in 10 seconds
DEF MAXANGA = MAXANGV/5
DEF MAXANGJERK = MAXANGA/5 #measure in radians/second^3
DEF GAPINC = 0.3 #if there's a gap on any side, move closest map offset towards edge
# Constants to be adjusted more often than those above:
DEF MAPS = 3 #12 #6
DEF FRAMES = 5000 #8000
DEF MAXSHRINK = 1.1 #1.2 is good for 3 maps. but too high for 12, it seems.
DEF MINSHRINK = 0.2
DEF LOOPSSINCEANYTHINGDRAWNLIMIT = 1000 #maybe 100 is enough?
#frame finished when the last this-many points were already drawn on at least MAXPTSPERPIXEL times.
DEF MAXPTSPERPIXEL = 10 #max number of points averaged to find col of each pixel
DEF MOVEINDIVISOR = 500 #200
DEF PICFOLDER = "/Users/admin/ifs/" #NB create this folder before running program
DEF LOGFILE = "/Users/admin/ifs-log.txt"
#each map has 2 equations, basically this:
#eq[0]: x = ax+by+c : a=.coef[0],b=.coef[1],c=.const
#eq[1]: y = dx+ey+f : d=.coef[0],e=.coef[1],f=.const
#with an added rotation term, making :
#eq[0]: x = axcos(ang) - bysin(ang) + c
#eq[1]: y = dxsin(ang) + eycos(ang) + f
cdef:
struct eqstruct:
float const, constv, coef[COEFFS], v[COEFFS]
struct mapstruct:
eqstruct eq[EQNS]
float angle, angularv, angulara
mapstruct map[MAPS]
struct dirs:
int left, right, top, bottom
dirs mostPt #i.e. x-coord of leftmost point in image etc
struct px:
short rgb[3], pts # = num of points' data added together in that pixel
px image[SCREENWID][SCREENHEI]
int loopsdone, loopsSinceAnythingDrawn
short out[2], colpowers[8]
float pt[2],p[MAPS],ptotal,pcumulative[MAPS]
enum:
ALLGOOD = 0, WENTOFFSCREEN = 1, PLUS = 1, MINUS = -1
InXEqn = 0, InYEqn = 1, X = 0,Y = 1
colpowers[:] = [128,64,32,16,8,4,2,1]
txt = open(LOGFILE, "w")
cdef SetInitialValues():
cdef short m,e,t
cdef float me[COLOURS][2][4]
for m in xrange(MAPS):
map[m].angle = StepRand(PI)
map[m].angularv = StepRand(MAXANGV)
map[m].angulara = StepRand(MAXANGA)
#initial fractal= a rough tiny sierpinski tri, doesnt really matter what it is.
me[0][0][:] = [-40,1,0.5,0]
me[0][1][:] = [-20,-1,0,0.5]
me[1][0][:] = [40,1,0.5,0]
me[1][1][:] = [-20,-1,0,0.5]
me[2][0][:] = [0,0,0.5,0]
me[2][1][:] = [30,1,0,0.5]
for m in xrange(MAPS):
for e in xrange(EQNS):
map[m].eq[e].const = me[m%COLOURS][e][0] #i.e. puts the same values in map 1,4,7,10 etc etc
map[m].eq[e].constv = me[m%COLOURS][e][1]
map[m].eq[e].coef[:] = [me[m%COLOURS][e][2],me[m%COLOURS][e][3]]
map[m].eq[e].v[:] = [0.01,0.01]
cdef float StepRand(float max): #returns num between -max and max. step size=that range divided by 200
return -max + (rand() % 401)*max/200
cdef float fsign(float num): #sign of floating pt var: 1, -1 or 0
if num < 0:
return -1
else:
return num > 0
cdef KeepVariablesWithinLimits():
cdef:
short m,e,t
float s
eqstruct *ME
for m in xrange(MAPS):
for e in xrange(EQNS):
map[m].angle = map[m].angle%(2*PI) #keep angle value in -PI< <PI but dont change angle!!!
s = fsign(map[m].angularv)
if s*map[m].angularv > MAXANGV:
map[m].angulara = -s*MAXANGA*REBOUNDRATIO
ME = &map[m].eq[e]
s = fsign(ME.constv)
if s*ME.constv > MAXCONSTV:
ME.constv = s*MAXCONSTV
for t in xrange(COEFFS): #0,1 , i.e. x and y terms, not c
s=fsign(ME.coef[t])
if s*ME.coef[t]>MAXSHRINK: #if >max or <-max
ME.coef[t] = s*MAXSHRINK
ME.v[t] = -s*REBOUNDRATIO
if fabs(ME.v[t]) > MAXSHRINKV:
ME.v[t] = fsign(ME.v[t])*MAXSHRINKV
if s*ME.coef[t]<MINSHRINK: #if <min and >-min
ME.coef[t] = s*MINSHRINK
ME.v[t] = s*REBOUNDRATIO
cdef UpdateEquationValues():
cdef short m,e,t
for m in xrange(MAPS):
map[m].angularv += map[m].angulara
map[m].angle += map[m].angularv
for e in xrange(EQNS):
map[m].eq[e].const += map[m].eq[e].constv
for t in xrange(COEFFS):
map[m].eq[e].coef[t] += map[m].eq[e].v[t]
cdef CalculateMapChoiceWeights():
global ptotal
cdef short i
ptotal = 0
#the formula: using x=ax+by, y=dx+ey (ignoring constant and rotation)
#the image points of the triangle (0,1), (0,0) and (1,0), are
#(b,e), (0,0) and (a,d). The area of this triangle is |bd-ae|/2.
#Since only the ratios between maps are important, just use |bd-ae|
for i in xrange(MAPS):
p[i] = fabs(map[i].eq[0].coef[1]*map[i].eq[1].coef[0]-map[i].eq[0].coef[0]*map[i].eq[1].coef[1])
ptotal += p[i]
pcumulative[i] = ptotal
cdef short ChooseAMap():
cdef short i,ir = rand() % <int>(ptotal*10000+1)
cdef float r = (<float>ir)/10000
for i in xrange(MAPS):
if r <= pcumulative[i]:
return i #i think but not sure that this ALWAYS works. otherwise:
return MAPS-1
cdef CalculateNewPoint(short m):
cdef float tempx, cosa = cos(map[m].angle), sina = sin(map[m].angle)
tempx = pt[X]*cosa*map[m].eq[0].coef[0] - pt[Y]*sina*map[m].eq[0].coef[1] + map[m].eq[0].const
pt[Y] = pt[X]*sina*map[m].eq[1].coef[0] + pt[Y]*cosa*map[m].eq[1].coef[1] + map[m].eq[1].const
pt[X] = tempx
cdef short TryMillionsOfLoops(): # it's a different num of loops every time.
global loopsdone, loopsSinceAnythingDrawn
cdef:
short col,i,intpt[2],m, last8[8], RGB[3]
int count
loopsSinceAnythingDrawn = 0
while loopsSinceAnythingDrawn < LOOPSSINCEANYTHINGDRAWNLIMIT:
count += 1
if count > MAXLOOPS:
break
m = ChooseAMap()
CalculateNewPoint(m)
intpt[:] = [<short> pt[X] + SCREENWID/2 , <short> pt[Y] + SCREENHEI/2]
for i from 6 >= i >= 0:
last8[i+1] = last8[i]
last8[0] = m%COLOURS
if count < 10:
continue #dont draw first 10 values
if intpt[X] >= SCREENWID or intpt[X] < 0 or intpt[Y] >= SCREENHEI or intpt[Y] < 0:
out[:] = intpt
return WENTOFFSCREEN #things arent All Good.
if image[intpt[X]][intpt[Y]].pts < MAXPTSPERPIXEL:
RGB[:] = [0,0,0]
for i in xrange(8):
RGB[last8[i]] += colpowers[i] #0->128,1->64,2->32,3->16,4->8,5->4,6->2,7->1
for col in xrange(3):
image[intpt[X]][intpt[Y]].rgb[col] += RGB[col]
image[intpt[X]][intpt[Y]].pts += 1
loopsSinceAnythingDrawn = 0 #reset counter as point was drawn to
else: loopsSinceAnythingDrawn += 1
if intpt[X] > mostPt.right: mostPt.right = intpt[X]
if intpt[X] < mostPt.left: mostPt.left = intpt[X]
if intpt[Y] > mostPt.top: mostPt.top = intpt[Y]
if intpt[Y] < mostPt.bottom: mostPt.bottom = intpt[Y]
loopsdone = count #save count to write to logfile later
return ALLGOOD
cdef short MapWithGreatestConst(short equ): #in equations equ (i.e. in X eqs or Y eqs)
cdef short m,greatest = 0
cdef float maxval = map[0].eq[equ].const #assume map[0] is greatest..
for m in xrange(1,MAPS):
if map[m].eq[equ].const > maxval:
greatest = m
maxval = map[m].eq[equ].const
return greatest
cdef short MapWithSmallestConst(short equ):
cdef short m,smallest = 0
cdef float minval = map[0].eq[equ].const #assume map[0] is least..
for m in xrange(1,MAPS):
if map[m].eq[equ].const < minval:
smallest = m
minval = map[m].eq[equ].const
return smallest
cdef AddToGreatestConst(float ch, short equ):
map[MapWithGreatestConst(equ)].eq[equ].const += ch
cdef AddToSmallestConst(float ch,short equ):
map[MapWithSmallestConst(equ)].eq[equ].const += ch
cdef MoveConstsInward(float xr, float yr):
cdef short m,e,t
if out[X] >= SCREENWID: #i.e. if 100, move in by 0.5; if 400, move in by 2
AddToGreatestConst(-xr/MOVEINDIVISOR,InXEqn)
if out[X] < 0:
AddToSmallestConst(xr/MOVEINDIVISOR,InXEqn)
if out[Y] >= SCREENHEI:
AddToGreatestConst(-yr/MOVEINDIVISOR,InYEqn)
if out[Y] < 0:
AddToSmallestConst(yr/MOVEINDIVISOR,InYEqn)
cdef ReduceLargestLinearCoefficient(): #find max shrink val..
cdef float maxshrinkval = 0, sign
cdef short maxterm,m,e,t,maxm,maxe
for m in xrange(MAPS):
for e in xrange(EQNS):
for t in xrange(COEFFS):
if fabs(map[m].eq[e].coef[t]) > maxshrinkval:
maxm = m
maxe = e
maxshrinkval = fabs(map[m].eq[e].coef[t])
maxterm = t
sign = fsign(map[maxm].eq[maxe].coef[maxterm])
map[maxm].eq[maxe].coef[maxterm] -= sign*SHRINKSTEP
cdef float ConstRange(short equ):
return map[MapWithGreatestConst(equ)].eq[equ].const-map[MapWithSmallestConst(equ)].eq[equ].const
cdef MakeFractalSmaller():
cdef short ConstsAreDecentlySpreadOut
cdef float xrange = ConstRange(X), yrange = ConstRange(Y)
ConstsAreDecentlySpreadOut = xrange*yrange > 10000 #hmm 100x100, not much..
print "xrange*yrange: %f x %f = %f ? %d" % (xrange,yrange,xrange*yrange,ConstsAreDecentlySpreadOut)
if ConstsAreDecentlySpreadOut:
if rand()%2: # 50% chance
MoveConstsInward(xrange,yrange)
return
ReduceLargestLinearCoefficient()
cdef ShiftAllXorYConstants(short XorY, float sign):
cdef short i
for i in xrange(MAPS):
map[i].eq[XorY].const += sign*GAPINC #lil bit
cdef MakeFractalMoreCentred():
cdef short i, LeftGap = mostPt.left, RightGap = SCREENWID-mostPt.right
cdef short TopGap = SCREENHEI-mostPt.top, BottomGap = mostPt.bottom
#if a bigger (by > MARGIN) gap on an edge, move nearest map const closer to that edge
if LeftGap > MARGIN+RightGap:
ShiftAllXorYConstants(InXEqn,MINUS)
elif RightGap > MARGIN+LeftGap:
ShiftAllXorYConstants(InXEqn,PLUS)
if TopGap > MARGIN+BottomGap:
ShiftAllXorYConstants(InYEqn,PLUS)
elif BottomGap > MARGIN+TopGap:
ShiftAllXorYConstants(InYEqn,MINUS)
cdef RandomlyAlterOneValueInEachEquation():
cdef short m,e,t
for m in xrange(MAPS):
map[m].angulara += StepRand(MAXANGJERK)
for e in xrange(EQNS):
t = rand()%(COEFFS+1)
if t == COEFFS: #ie 2
map[m].eq[e].constv += StepRand(MAXCONSTA)
continue #else if t == 0 or t == 1:
map[m].eq[e].v[t] += StepRand(MAXSHRINKA)
cdef WriteToLogFile(short n):
cdef short m
txt.write("Frame no. %d - loops: %d\n" % (n,loopsdone))
for m in xrange(MAPS):
txt.write("Map %d a b c\n" % m)
txt.write("x: %f %f %f v: %f %f %f\n" % (map[m].eq[0].coef[0],
map[m].eq[0].coef[1],map[m].eq[0].const,map[m].eq[0].v[0],map[m].eq[0].v[1],map[m].eq[0].constv))
txt.write("y: %f %f %f v: %f %f %f\n" % (map[m].eq[1].coef[0],
map[m].eq[1].coef[1],map[m].eq[1].const,map[m].eq[1].v[0],map[m].eq[1].v[1],map[m].eq[1].constv))
txt.write("angle: %f angularv: %f angulara: %f\n" % (map[m].angle,map[m].angularv,map[m].angulara))
txt.write("mostPt.left: %d R: %d T: %d B: %d\n" % (mostPt.left,mostPt.right,mostPt.top,mostPt.bottom))
txt.write("ptotal=%f\n" % ptotal)
for m in xrange(MAPS):
txt.write("p[%d]=%f p[%d]/ptotal=%f\n" % (m,p[m],m,p[m]/ptotal))
txt.write("-------------------------------------\n")
cdef DisplayOutOfBoundsData(short picnum):
cdef short m,e,t
print "\nOUT of BOUNDS frame not saved = ",picnum
for m in xrange(MAPS):
for e in xrange(EQNS):
print "map[%d].eq[%d].const: %f v: %f" % (m,e,map[m].eq[e].const,map[m].eq[e].constv)
print "coef[0]: %f v: %f coef[1]: %f v: %f" % (map[m].eq[e].coef[0],map[m].eq[e].v[0],map[m].eq[e].coef[1],map[m].eq[e].v[1])
cdef SetupMainLoopVars():
cdef short i, j
for i in xrange(SCREENWID):
for j in xrange(SCREENHEI):
image[i][j].pts = 0
image[i][j].rgb[:] = [0,0,0]
pt[:] = [SCREENWID/2,SCREENHEI/2] #initial point. doesnt matter what it is.
mostPt.left = SCREENWID
mostPt.right = 0
mostPt.top = 0
mostPt.bottom = SCREENHEI
CalculateMapChoiceWeights()
cpdef OK():
cdef:
np.ndarray[np.uint8_t,ndim = 3] pic = np.empty((SCREENHEI,SCREENWID,3),dtype = np.uint8)
short i,j,col,frame = 0, picnum = 0
SetInitialValues()
while True: #done like this because not every loop makes an image.
SetupMainLoopVars()
if TryMillionsOfLoops() == WENTOFFSCREEN: #then dont save
MakeFractalSmaller()
DisplayOutOfBoundsData(picnum)
continue #start again; try another IFS with new vals
for i in xrange(SCREENWID):
for j in xrange(SCREENHEI):
if image[i][j].pts > 0: #number of pts stored 'in' that pixel
for col in xrange(3):
pic[SCREENHEI-1-j,i,col] = image[i][j].rgb[col]/image[i][j].pts #swaps x and y..
else: #not used, make black.
for col in xrange(3):
pic[SCREENHEI-1-j,i,col] = 0
name = '{}{:05}.png'.format(PICFOLDER,picnum)
misc.imsave(name,pic)
print picnum
WriteToLogFile(picnum)
picnum += 1
if picnum > FRAMES:
break
MakeFractalMoreCentred()
RandomlyAlterOneValueInEachEquation()
UpdateEquationValues()
KeepVariablesWithinLimits()
txt.close()
Program notes
1. None of the ‘shrink’ values (a,b,d,e) are ever negative – making a flipped-over fractal – which is a shame. I couldn’t think of a good way of doing that, as they can’t get too close to 0. Except starting some of them off negative, maybe a good idea. I think the program will correctly deal with neg values as is, i.e. they will stay negative, between -0.2 and -1.1 with current settings.
2. Things are added to the angular acceleration, but to the velocity of the shrink (a,b,d,e) and location (c and f). There’s no reason for that, I just didn’t get around to adding jerk (rate of change of acceleration) to the others.
3. I tried with more than 3 maps, 12 also works well. (A minimum shrink of 0.1 seems better with large numbers of maps) Any number can be used >=3. The colours R,G,B are re-used for colours mod 3. Maybe 2 maps works also, I didn’t try that.
4. Things move too fast if anything; it should be slowed down a bit. Slowed down a lot more, 25 fps movies would be better than 15 I guess, but 15 looks fine.
p.s. I was reading Refactoring and Clean Code (both wonderful programming books) while working on this; I hope it shows. 🙂
