Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
GIF art in Python

I'm reading through this blog post, which is about creating cool vector animations in Python using the libraries
gizeh
andmoviepy
, neither of which have any hope of working in pythonista. That said, many of the same effects can be achieved in pythonista using images2gif and PIL. I've achieved a good result with the first example I tried to convert.Their code:
import numpy as np import gizeh import moviepy.editor as mpy W,H = 128,128 duration = 2 ncircles = 20 # Number of circles def make_frame(t): surface = gizeh.Surface(W,H) for i in range(ncircles): angle = 2*np.pi*(1.0*i/ncircles+t/duration) center = W*( 0.5+ gizeh.polar2cart(0.1,angle)) circle = gizeh.circle(r= W*(1.01.0*i/ncircles), xy= center, fill= (i%2,i%2,i%2)) circle.draw(surface) return surface.get_npimage() clip = mpy.VideoClip(make_frame, duration=duration) clip.write_gif("circles.gif",fps=15, opt="OptimizePlus", fuzz=10)
outputs
And my code:
# coding: utf8 import numpy as np from PIL import Image, ImageDraw from images2gif import writeGif from math import sin,cos W,H = 128,128 duration = 2 ncircles = 20 # Number of circles def polar2cart(r,theta): x = r*cos(theta) y = r*sin(theta) return x, y def make_frame(t): im = Image.new('RGB', (W,H), (0,0,0)) surface = ImageDraw.Draw(im) for i in range(ncircles): angle = 2*np.pi*(1.0*i/ncircles+t/duration) center = W*( 0.5 + polar2cart(0.1,angle)[0]), W*( 0.5 + polar2cart(0.1,angle)[1]) r = W*(1.01.0*i/ncircles) bbox = (center[0]r, center[1]r, center[0]+r, center[1]+r) surface.ellipse(bbox, fill= (i%2*255,i%2*255,i%2*255)) del surface return im images = [] for x in range(200): images.append(make_frame(x/25.0)) writeGif('tunnelswirl.gif',images,0.005)
outputs
Ok, so theirs is prettier because it's vector art and mine is a 128x128 PIL image, but I achieved the same effect. My code is (very heavily) based off of theirs, but it wasn't easy for me to do. I can achieve slightly cleaner results from PIL if I generate frames at 1024x1024, then resize to 128,128. I use this technique whenever I do font rendering with PIL. It would also be interesting to do this with the
canvas
module, which is for vector graphics.It also works with more circles

Very cool! I saw that blog post a while ago and always wanted to port some of it to Pythonista.

Thanks :)

Here's a version that uses the
ui
module for drawing the circles, so that they're antialiased – the result gets pretty close to the original, though I think the framerate is different.# coding: utf8 import numpy as np from PIL import Image, ImageDraw from images2gif import writeGif from math import sin,cos import ui import io W,H = 128,128 duration = 2 ncircles = 20 # Number of circles def polar2cart(r,theta): x = r*cos(theta) y = r*sin(theta) return x, y def ui2pil(ui_img): png_data = ui_img.to_png() return Image.open(io.BytesIO(png_data)) def make_frame(t): with ui.ImageContext(W, H, 1) as ctx: for i in range(ncircles): angle = 2*np.pi*(1.0*i/ncircles+t/duration) center = W*( 0.5 + polar2cart(0.1,angle)[0]), W*( 0.5 + polar2cart(0.1,angle)[1]) r = W*(1.01.0*i/ncircles) ui.set_color((i%2, i%2, i%2)) ui.Path.oval(center[0]r, center[1]r, r*2, r*2).fill() return ui2pil(ctx.get_image()) images = [] for x in range(200): images.append(make_frame(x/25.0)) writeGif('tunnelswirl.gif',images,0.005)

images = [make_frame(x/25.0) for x in xrange(200)] # ;)

@omz you need to change the last line to read
images.append(make_frame(x/25.0).convert('RGB'))
because alpha channels are not supported in images2gif

@Webmaster4o It worked fine for me, but maybe I have a different version of
images2gif
(I used this one). 
Yeah, I used the one I linked to in my original post. There are like 30 versions online. Yours is over 3 times the length of mine.

I'm torn between UI and PIL. UI looks so much better, but PIL will work on desktop as well as mobile.
EDIT:
I can achieve the same effect in PIL using an antialias filter on resize.Updated code:
# coding: utf8 import numpy as np from PIL import Image, ImageDraw from images2gif import writeGif from math import sin,cos W,H = 1024,1024 duration = 2 ncircles = 20 # Number of circles def polar2cart(r,theta): x = r*cos(theta) y = r*sin(theta) return x, y def make_frame(t): im = Image.new('RGB', (W,H), (0,0,0)) surface = ImageDraw.Draw(im) for i in range(ncircles): angle = 2*np.pi*(1.0*i/ncircles+t/duration) center = W*( 0.5 + polar2cart(0.1,angle)[0]), W*( 0.5 + polar2cart(0.1,angle)[1]) r = W*(1.01.0*i/ncircles) bbox = (center[0]r, center[1]r, center[0]+r, center[1]+r) surface.ellipse(bbox, fill= (i%2*255,i%2*255,i%2*255)) del surface return im.resize((256,256), Image.ANTIALIAS) images = [] for x in range(50): images.append(make_frame(x/25.0)) writeGif('tunnelswirl.gif',images,0.005)
Output:

I've tried to port the spinning hexagons to pythonista.
Code:
# coding: utf8 import colorsys, console import numpy as np from images2gif import writeGif from PIL import Image, ImageDraw sin, cos, pi = np.sin, np.cos, np.pi W,H = 1024,1024 NFACES = 5 #Number of faces on the polygon R = 0.3 #Radius of polygon NSQUARES = 100 # Number of squares DURATION = 1 def polar_polygon(nfaces,radius, npoints): """ Returns the (x,y) coordinates of n points regularly spaced along a regular polygon of `nfaces` faces and given radius. """ theta=np.linspace(0,2*np.pi,npoints)[:1] n = nfaces r= cos( pi/n )/cos((theta%(2*pi/n))pi/n) d = np.cumsum(np.sqrt(((r[1:]r[:1])**2))) d = [0]+list(d/d.max()) return zip(radius*r, theta, d) def polar2cart(r,theta): x = r*cos(theta) y = r*sin(theta) return x, y def squarecoords(sidelength, center, angle): cx, cy = center radius = sidelength/2 corners = [(cxradius,cyradius), (cx+radius,cyradius), (cx+radius,cy+radius), (cxradius,cy+radius)] def rotate(point, angle, center=(0, 0)): theta = angle*(np.pi/180.0) translated = point[0]center[0] , point[1]center[1] rotated = (translated[0]*cos(theta)translated[1]*sin(theta),translated[0]*sin(theta)+translated[1]*cos(theta)) newcoords = (round(rotated[0]+center[0], 1),round(rotated[1]+center[1], 1)) return newcoords newcorners = [] for x in corners: newcorners.append(rotate(x,angle,center)) return tuple([tuple([int(x) for x in y]) for y in newcorners]) def half(t, side="left"): points = polar_polygon(NFACES, R, NSQUARES) ipoint = 0 if side=="left" else NSQUARES/2 points = (points[ipoint:]+points[:ipoint])[::1] i = Image.new('RGB', (W, H), (0,0,0)) surface = ImageDraw.Draw(i) for (r, th, d) in points: center = W*(0.5+polar2cart(r,th)[0]),W*(0.5+polar2cart(r,th)[1]) #angle = (np.pi*d + t*np.pi/DURATION)*50 angle = (t*180) color= colorsys.hls_to_rgb((2*d+t/DURATION)%1,.5,.5) color = tuple([int(x*255) for x in color]) coords = squarecoords(0.17*W, center, angle) surface.polygon(coords, color, outline=(255,255,255)) im = np.asarray(i) return (im[:,:W/2] if (side=="left") else im[:,W/2:]) def make_frame(t): lefthalf = half(t,"left") righthalf = half(t,"right") return Image.fromarray(np.hstack((lefthalf, righthalf))) images = [] for x in range(100): images.append(make_frame(x/100.0).resize((512,512), Image.ANTIALIAS)) console.clear() print str(x+1)+'%' console.clear() print 'Writing gif...' writeGif("pentagon.gif", images, duration=0.01)
Note:
The original had a wave effect sort of:
I could not replicate this. The commented out line above where I declare angle is the line they used to achieve this, but I could not replicate the result. Feel free to try it.
As you can tell, this was not as successful as the last.

New animation based on this code:
Code:
# coding: utf8 import math from operator import itemgetter import console from images2gif import writeGif from PIL import Image, ImageDraw W, H = (1024, 1024) class Point3D: def __init__(self, x = 0, y = 0, z = 0): self.x, self.y, self.z = float(x), float(y), float(z) def rotateX(self, angle): """ Rotates the point around the X axis by the given angle in degrees. """ rad = angle * math.pi / 180 cosa = math.cos(rad) sina = math.sin(rad) y = self.y * cosa  self.z * sina z = self.y * sina + self.z * cosa return Point3D(self.x, y, z) def rotateY(self, angle): """ Rotates the point around the Y axis by the given angle in degrees. """ rad = angle * math.pi / 180 cosa = math.cos(rad) sina = math.sin(rad) z = self.z * cosa  self.x * sina x = self.z * sina + self.x * cosa return Point3D(x, self.y, z) def rotateZ(self, angle): """ Rotates the point around the Z axis by the given angle in degrees. """ rad = angle * math.pi / 180 cosa = math.cos(rad) sina = math.sin(rad) x = self.x * cosa  self.y * sina y = self.x * sina + self.y * cosa return Point3D(x, y, self.z) def project(self, win_width, win_height, fov, viewer_distance): """ Transforms this 3D point to 2D using a perspective projection. """ factor = fov / (viewer_distance + self.z) x = self.x * factor + win_width / 2 y = self.y * factor + win_height / 2 return Point3D(x, y, self.z) class Cube: def __init__(self, win_width = 640, win_height = 480): self.vertices = [ Point3D(1,1,1), Point3D(1,1,1), Point3D(1,1,1), Point3D(1,1,1), Point3D(1,1,1), Point3D(1,1,1), Point3D(1,1,1), Point3D(1,1,1) ] # Define the vertices that compose each of the 6 faces. These numbers are # indices to the vertices list defined above. self.faces = [(0,1,2,3),(1,5,6,2),(5,4,7,6),(4,0,3,7),(0,4,5,1),(3,2,6,7)] # Define colors for each face self.colors = [ '#FFEC94', '#FFAEAE', '#404040', '#B0E57C', '#B4D8E7', '#7BC8A4' ] def make_frame(self, angle): # It will hold transformed vertices. t = [] screen = Image.new('RGB',(W, H), (255,255,255)) draw = ImageDraw.Draw(screen) for v in self.vertices: # Rotate the point around X axis, then around Y axis, and finally around Z axis. r = v.rotateX(angle).rotateY(angle).rotateZ(angle) # Transform the point from 3D to 2D #if angle <= 180: # p = r.project(W, H, 256, 3+(angle/90.0)) #else: # p = r.project(W, H, 256, 5((angle180)/90.0)) p = r.project(W, H, 256, 3) # Put the point in the list of transformed vertices t.append(p) # Calculate the average Z values of each face. avg_z = [] i = 0 for f in self.faces: z = (t[f[0]].z + t[f[1]].z + t[f[2]].z + t[f[3]].z) / 4.0 avg_z.append([i,z]) i = i + 1 # Draw the faces using the Painter's algorithm: # Distant faces are drawn before the closer ones. for tmp in sorted(avg_z,key=itemgetter(1),reverse=True): face_index = tmp[0] f = self.faces[face_index] pointlist = [(t[f[0]].x, t[f[0]].y), (t[f[1]].x, t[f[1]].y), (t[f[1]].x, t[f[1]].y), (t[f[2]].x, t[f[2]].y), (t[f[2]].x, t[f[2]].y), (t[f[3]].x, t[f[3]].y), (t[f[3]].x, t[f[3]].y), (t[f[0]].x, t[f[0]].y)] draw.polygon(pointlist, fill=self.colors[face_index]) return screen.resize((512,512), Image.ANTIALIAS) c = Cube() images = [] for x in range(0,360,5): images.append(c.make_frame(x)) console.clear() print str((x/360.0)*100.0)[:5]+'%' writeGif('cube.gif', images, 0.01)

Duration = 1
should be
Duration = 1.0
there are a few other places where you intended floats, but wrote ints, although i am not aure if any of those caused issues.
or you can use
from __future__ import division
edit: on second look, the problem is that the polygon function expects degrees, not radians. The commented line is.... not quite either (unless the intention was to use a number near 180, but nonrepeating)
try
angle = (N1.*d+N2*t/DURATION)*360.0
playing with those two factors will control, respectively, how many revolutions a square gets going around the polygon in a single image, and how many revolutions a square gets over the animation. You might try 0.5 for both, and play with these in integer 1/2 steps, the numbers can be different.

New sierpinski fractal animation:
Code is sloppy, I'll probably clean it up later:
# coding: utf8 from images2gif import writeGif from PIL import Image, ImageDraw, ImageChops import console W, H = 1024,1024 RESOLUTION = 5 FG, BG = '#ffbb00', '#009bff' #FG, BG = '#000000', '#ffffff' def drange(start, stop, step=1): n = int(round((stop  start)/float(step))) if n > 1: return([start + step*i for i in range(n+1)]) else: return([]) def irange(start, increments): mylist = [start] for i in increments: mylist.append(mylist[1]+i) return mylist class Rect: def __init__(self, left, top, width, height): self.left = left self.right = left + width self.top = top self.bottom = top+height self.centerx = left + (width/2) self.centery = top + (height/2) self.width = width self.height = height self.bbox = (left, top), (self.right, self.bottom) def drawSierpinski(surf, rect, fgcolor=FG, bgcolor=BG, level=6, topshade=True): try: rect.left except AttributeError: left, top, width, height = rect rect = Rect(left, top, width, height) if level == 0: return quarterWidth = (rect.width/4)+rect.left threeQuarterWidth = (rect.width/4*3)+rect.left topRect = Rect(quarterWidth, rect.top, (rect.width / 2), (rect.height / 2)) leftRect = Rect(rect.left, rect.centery, (rect.width/2), (rect.height/2)) rightRect = Rect(rect.centerx, rect.centery, (rect.width / 2), (rect.height / 2)) #Shade topleft if topshade: surf.rectangle((rect.left, rect.top, rect.centerx, rect.bottom), fill=bgcolor) #outer triangle surf.polygon([(rect.centerx,rect.top),(rect.left,rect.bottom),(rect.right,rect.bottom)],fgcolor) #inner upsidedown triangle surf.polygon([(quarterWidth,rect.centery),(rect.centerx,rect.bottom),(threeQuarterWidth, rect.centery)],bgcolor) #do recursive calls drawSierpinski(surf, topRect, fgcolor, bgcolor, level1, topshade) drawSierpinski(surf, leftRect, fgcolor, bgcolor, level1, topshade) drawSierpinski(surf, rightRect, fgcolor, bgcolor, level1, topshade) return im def make_sierpinski(level, topshade=True): im = Image.new('RGB', (W, H), (255,255,255)) surf = ImageDraw.Draw(im) drawSierpinski(surf, Rect(0, 0, W, H), FG, BG, level, topshade) return im im = Image.new('RGB', (W, H), (255,255,255)) surf = ImageDraw.Draw(im) sierpinski1 = make_sierpinski(RESOLUTION) sierpinski2 = make_sierpinski(RESOLUTION+1) numbers = [int(n) for n in irange(0, drange(8,4,0.0475))] images = [] for x in numbers: a = sierpinski1.crop((x, x, W, W)).resize((512, 512), Image.ANTIALIAS) b = sierpinski2.crop((x, x, W, W)).resize((512, 512), Image.ANTIALIAS) alpha = x / float(W/2) images.append(Image.blend(a, b, alpha)) console.clear() print str(alpha*100)+'%' console.clear() print 'writing...' writeGif('sierpinski.gif', images, 2.0/len(images))```
