Gizeh (that is, Cairo for tourists) is a great interface to the Cairo drawing library.

I recently wished to make a small animation of a bar moving in the visual field and crossing a simple receptive field to illustrate some simple motions that could be captured in the primary visual cortex ansd experiments that could be done there.

Let's first draw a simple receptive field as a disk filled with a radial gradient:

In [1]:
!pip install git+https://github.com/Zulko/gizeh.git
Collecting git+https://github.com/Zulko/gizeh.git
  Cloning https://github.com/Zulko/gizeh.git to /var/folders/3p/m0g52j9j69z3gj8ktpgg1dm00000gn/T/pip-wiha44xz-build
  Requirement already satisfied (use --upgrade to upgrade): gizeh==0.1.10 from git+https://github.com/Zulko/gizeh.git in /usr/local/lib/python3.5/site-packages
Requirement already satisfied (use --upgrade to upgrade): cairocffi in /usr/local/lib/python3.5/site-packages (from gizeh==0.1.10)
Requirement already satisfied (use --upgrade to upgrade): numpy in /usr/local/lib/python3.5/site-packages (from gizeh==0.1.10)
Requirement already satisfied (use --upgrade to upgrade): cffi>=1.1.0 in /usr/local/lib/python3.5/site-packages (from cairocffi->gizeh==0.1.10)
Requirement already satisfied (use --upgrade to upgrade): pycparser in /usr/local/lib/python3.5/site-packages (from cffi>=1.1.0->cairocffi->gizeh==0.1.10)
In [2]:
import gizeh as gz
W, H = 500, 300
r, gray, t = 25., .3, 1.

surface = gz.Surface(W,H, bg_color=(1, 1, 1)) # white background
gradient = gz.ColorGradient(type="radial", stops_colors=[(0,(gray, gray, gray)), (1, (1, 1, 1))],
                            xy1=[0, 0], xy2=[0, 0], xy3=[0, r])
rf = gz.circle(r=r, xy=(W/2., H/2.), fill=gradient)
#rect = gz.rectangle(lx=.3*H, ly=.02*H, xy=(W*t/duration, H/2.), fill=(0,1,0), angle=np.pi/2)

rf.draw(surface)
surface.ipython_display()
Out[2]:

filling with a RadialGradient

Looks easy, however, this took a little searching and tweaking. Finally found some documentation @ http://docs.go-mono.com/index.aspx?link=T%3aCairo.RadialGradient about the RadialGradient class:

Extends Cairo.Gradient to create a new radial gradient between the two circles defined by (cx0, cxy0, radius0) and (cx1, cy1, radius1).

such that we should have the following equivalence between Gizeh and Cairo

xy1, xy2, xy3 = (cx0, cy0), (cx1, cy1), (radius0, radius1)

a couple of MWE (AKA Mimimum Working Example) for each different parameter should be more helpful to see what happens:

In [3]:
import gizeh as gz
import numpy as np

W, H = 500, 300
r, gray = 25., .3

surface = gz.Surface(W,H, bg_color=(1,1,0)) # yellow background

N = 5
for x, radius1 in zip(np.linspace(W/8., 7*W/8., N), np.logspace(-2, 2, N, base=2)*r):
    gradient = gz.ColorGradient(type="radial", stops_colors=[(0,(gray, gray, gray)), (1, (1, 1, 1))],
                                xy1=[0, 0], xy2=[0, 0], xy3=[0, radius1])
    gz.circle(r=r, xy=(x, H/2.), fill=gradient).draw(surface)
surface.ipython_display()
Out[3]:
In [4]:
surface = gz.Surface(W,H, bg_color=(1,1,0)) # yellow background
for x, cx0 in zip(np.linspace(W/8., 7*W/8., N), np.linspace(0, 2*r, N)):
    gradient = gz.ColorGradient(type="radial", stops_colors=[(0,(gray, gray, gray)), (1, (1, 1, 1))],
                                xy1=[cx0, 0], xy2=[0, 0], xy3=[0, r])
    gz.circle(r=r, xy=(x, H/2.), fill=gradient).draw(surface)
surface.ipython_display()
Out[4]:
In [5]:
surface = gz.Surface(W,H, bg_color=(1,1,0)) # yellow background
for x, radius0 in zip(np.linspace(W/8., 7*W/8., N), np.logspace(-2, 2, N, base=2)*r):
    gradient = gz.ColorGradient(type="radial", stops_colors=[(0,(gray, gray, gray)), (1, (1, 1, 1))],
                                xy1=[0, 0], xy2=[0, 0], xy3=[radius0, r])
    gz.circle(r=r, xy=(x, H/2.), fill=gradient).draw(surface)
surface.ipython_display()
Out[5]:
In [6]:
surface = gz.Surface(W,H, bg_color=(1,1,0)) # yellow background
for x, cy0 in zip(np.linspace(W/8., 7*W/8., N), np.linspace(0, 2*r, N)):
    gradient = gz.ColorGradient(type="radial", stops_colors=[(0,(gray, gray, gray)), (1, (1, 1, 1))],
                                xy1=[0, cy0], xy2=[0, 0], xy3=[0, r])
    gz.circle(r=r, xy=(x, H/2.), fill=gradient).draw(surface)
surface.ipython_display()
Out[6]:
In [7]:
surface = gz.Surface(W,H, bg_color=(1,1,0)) # yellow background
for x, cy1 in zip(np.linspace(W/8., 7*W/8., N), np.linspace(0, 2*r, N)):
    gradient = gz.ColorGradient(type="radial", stops_colors=[(0,(gray, gray, gray)), (1, (1, 1, 1))],
                                xy1=[0, 0], xy2=[0, cy1], xy3=[0, r])
    gz.circle(r=r, xy=(x, H/2.), fill=gradient).draw(surface)
surface.ipython_display()
Out[7]:

Making an animation

And now the animation for the straight trajectory of a segment with perpendicular orientation:

In [8]:
import numpy as np
import gizeh as gz
import moviepy.editor as mpy

W, H = 500, 300
duration = 3
r, color = 25., .1
figpath = '/tmp/'
fps = 25

gradient = gz.ColorGradient(type="radial", stops_colors = [(0,(color,color,color)), (1,(1,1,1))],
                            xy1=[0,0], xy2=[0,0], xy3 = [0, r])
rf = gz.circle(r=r, xy=(W/2., H/2.), fill=gradient)

def make_frame(t):

    surface = gz.Surface(W,H, bg_color=(1,1,1))
    rf.draw(surface)

    rect = gz.rectangle(lx=.3*H, ly=.02*H, xy=(W*t/duration, H/2.), fill=(0,1,0), angle=np.pi/2)
    rect.draw(surface)

    return surface.get_npimage()

clip = mpy.VideoClip(make_frame, duration=duration)
clip.write_videofile(figpath + 'trax_0.mp4', fps=fps) # Many options...

clip.ipython_display(fps=fps, width=W, autoplay=1, loop=1)
[MoviePy] >>>> Building video /tmp/trax_0.mp4
[MoviePy] Writing video /tmp/trax_0.mp4
                                                
[MoviePy] Done.
[MoviePy] >>>> Video ready: /tmp/trax_0.mp4 


Out[8]:

A straight trajectory of a segment with a slanted orientation:

In [9]:
def make_frame(t):
    surface = gz.Surface(W,H, bg_color=(1,1,1))
    rf.draw(surface)
    rect = gz.rectangle(lx=.3*H, ly=.02*H, xy=(W*t/duration, H/2.), fill=(0,1,0), angle=np.pi/3)
    rect.draw(surface)
    return surface.get_npimage()

clip = mpy.VideoClip(make_frame, duration=duration)
clip.write_videofile(figpath + 'trax_1.mp4', fps=fps) # Many options...

clip.ipython_display(fps=fps, width=W, autoplay=1, loop=1)
[MoviePy] >>>> Building video /tmp/trax_1.mp4
[MoviePy] Writing video /tmp/trax_1.mp4
                                                
[MoviePy] Done.
[MoviePy] >>>> Video ready: /tmp/trax_1.mp4 


Out[9]:

A straight trajectory of a segment with an ondulating orientation:

In [10]:
def make_frame(t):
    surface = gz.Surface(W,H, bg_color=(1,1,1))
    rf.draw(surface)
    modul = 1 - .2*np.sin(2*np.pi*t/duration)
    rect = gz.rectangle(lx=.3*H, ly=.02*H, xy=(W*t/duration, H/2.), fill=(0,1,0), angle=np.pi/2*modul)
    rect.draw(surface)
    return surface.get_npimage()

clip = mpy.VideoClip(make_frame, duration=duration)
clip.write_videofile(figpath + 'trax_2.mp4', fps=fps) # Many options...
clip.ipython_display(fps=fps, width=W, autoplay=1, loop=1)
[MoviePy] >>>> Building video /tmp/trax_2.mp4
[MoviePy] Writing video /tmp/trax_2.mp4
                                                
[MoviePy] Done.
[MoviePy] >>>> Video ready: /tmp/trax_2.mp4 


Out[10]:

An ondulating trajectory of a segment with an ondulating orientation, and with the orientation always perpentdicular to the orientation:

In [11]:
def make_frame(t):
    surface = gz.Surface(W,H, bg_color=(1,1,1))
    rf.draw(surface)
    modul = 1 - .2*np.sin(2*np.pi*t/duration)
    ymodul = 1 + .2*(np.cos(2*np.pi*t/duration)+1)
    rect = gz.rectangle(lx=.3*H, ly=.02*H, xy=(W*t/duration, H/2.*ymodul), fill=(0,1,0), angle=np.pi/2*modul)
    rect.draw(surface)
    return surface.get_npimage()

clip = mpy.VideoClip(make_frame, duration=duration)
clip.write_videofile(figpath + 'trax_3.mp4', fps=fps) # Many options...
clip.ipython_display(fps=fps, width=W, autoplay=1, loop=1)
[MoviePy] >>>> Building video /tmp/trax_3.mp4
[MoviePy] Writing video /tmp/trax_3.mp4
                                                
[MoviePy] Done.
[MoviePy] >>>> Video ready: /tmp/trax_3.mp4 


Out[11]:

Voilà!