This example loads images from a directory and displays them in a rotating ellipse. You may click an image to bring it to the front. When the image has rotated to the front it will move up while increasing in size, and the file path will appear at the top of the window.
This is larger than the examples used so far, with multiple timelines and multiple behaviours affecting multiple actors. However, it's still a relatively simple example. A real application would need to be more flexible and have more functionality.
TODO: Make this prettier. Use containers to do that.
File: main.py
import os
import sys
import clutter
# For showing the filename
label_filename = None
# For rotating all images around an ellipse
timeline_rotation = None
# For moving one image up and scaling it
timeline_moveup = None
behaviour_scale = None
behaviour_path = None
behaviour_opacity = None
# The y position of the ellipse of images
ELLIPSE_Y = 390
# The distance from front to back when it's rotated 90 degrees.
ELLIPSE_HEIGHT = 450
IMAGE_HEIGHT = 100
angle_step = 30
class Item(object):
def __init__(self, actor, filepath, ellipse_behaviour=None):
super(Item, self).__init__()
self.actor = actor
self.filepath = filepath
self.ellipse_behaviour = ellipse_behaviour
def __eq__(self, other):
return self.filepath == other.filepath
def __ne__(self, other):
return not self.__eq__(other)
item_at_front = None
list_items = []
def on_foreach_clear_list_items(data, user_data):
"""Not necessary in Python"""
def scale_texture_default(texture):
pixbuf_height = texture.get_size()[1]
scale = IMAGE_HEIGHT / float(pixbuf_height) if pixbuf_height else 0
texture.set_scale(scale, scale)
def load_images(directory_path):
# Clear any existing images
if not os.path.exists(directory_path):
return
# Clear any existing images
global list_items
list_items = []
# Discover the images in the directory
for image in os.listdir(directory_path):
if not image.endswith('.jpg'):
continue
path = os.path.join(directory_path, image)
# Try to load the file as an image
actor = clutter.Texture(path)
if actor:
item = Item(actor, path)
# Make sure that all images are shown with the same height
scale_texture_default(item.actor)
list_items.append(item)
def add_to_ellipse_behaviour(timeline_rotation, start_angle, item):
if not timeline_rotation:
return
alpha = clutter.Alpha(timeline_rotation, clutter.EASE_OUT_SINE)
item.ellipse_behaviour = clutter.BehaviourEllipse(alpha=alpha,
x=320, y=ELLIPSE_Y, # x, y
width=ELLIPSE_HEIGHT, height=ELLIPSE_HEIGHT, # width, height
start=start_angle, end=start_angle + 360)
item.ellipse_behaviour.set_direction(clutter.ROTATE_CW)
item.ellipse_behaviour.set_angle_tilt(clutter.X_AXIS, -90)
item.ellipse_behaviour.apply(item.actor)
def add_image_actors(stage):
x, y, angle = 20, 0, 0
global list_items
global timeline_rotation
for item in list_items:
# Add the actor to the stage
stage.add(item.actor)
# Set an initial position
item.actor.set_position(x, y)
y += 100
# Allow the actor to emit events. By default only the stage does this.
item.actor.set_reactive(True)
# Connect signal handlers for events
item.actor.connect('button-press-event', on_texture_button_press, item)
add_to_ellipse_behaviour(timeline_rotation, angle, item)
angle += angle_step
item.actor.show()
def angle_in_360(angle):
result = angle
while result >= 360:
result -= 360
return result
# This signal handler is called when the item has finished
# moving up and increasing in size.
def on_timeline_moveup_completed(timeline):
global timeline_moveup, behaviour_scale, behaviour_path, behaviour_opacity
timeline_moveup = behaviour_scale = None
behaviour_path = behaviour_opacity = None
# This signal handler is called when the items have completely
# rotated around the ellipse.
def on_timeline_rotation_completed(timeline):
# All the items have now been rotated so that the clicked item is at the
# front. Now we transform just this one item gradually some more, and
# show the filename.
# Transform the image
global timeline_moveup
actor = item_at_front.actor
timeline_moveup = clutter.Timeline(1000) # milliseconds
alpha = clutter.Alpha(timeline_moveup, clutter.EASE_OUT_SINE)
# Scale the item from its normal scale to approximately twice the normal scale
scale_start = actor.get_scale()[0]
scale_end = scale_start * 1.8
global behaviour_scale
behaviour_scale = clutter.BehaviourScale(scale_start, scale_start,
scale_end, scale_end,
alpha=alpha)
behaviour_scale.apply(actor)
# Move the item up the y axis
knots = (
(int(actor.get_x()), int(actor.get_y())),
(int(actor.get_x()), int(actor.get_y() - 250)),
)
global behaviour_path
behaviour_path = clutter.BehaviourPath(alpha, knots=knots)
behaviour_path.apply(actor)
# Show the filename gradually
global label_filename
label_filename.set_text(item_at_front.filepath)
global behaviour_opacity
behaviour_opacity = clutter.BehaviourOpacity(0, 255, alpha=alpha)
behaviour_opacity.apply(label_filename)
# Start the timeline and handle its "completed" signal so we can unref it
timeline_moveup.connect('completed', on_timeline_moveup_completed)
timeline_moveup.start()
def rotate_all_until_item_is_at_front(item):
if not item:
return
global timeline_rotation
timeline_rotation.stop()
# Stop the other timeline in case that is active at the same time
global timeline_moveup
if timeline_moveup:
timeline_moveup.stop()
global label_filename
label_filename.set_opacity(0)
# Get the item's position in the list
global list_items
pos = list_items.index(item)
assert pos != -1
global item_at_front
if not item_at_front and list_items:
item_at_front = list_items[0]
pos_front = 0
if item_at_front:
pos_front = list_items.index(item_at_front)
assert pos_front != -1
# pos_offset_before_start = pos_front - pos
# Calculate the end angle of the first item
angle_front = 180
angle_start = angle_front - (angle_step * pos_front)
angle_start = angle_in_360(angle_start)
angle_end = angle_front - (angle_step * pos)
angle_diff = 0
# Set the end angles
for this_item in list_items:
# Reset its size
scale_texture_default(this_item.actor)
angle_start = angle_in_360(angle_start)
angle_end = angle_in_360(angle_end)
# Move 360 instead of 0
# when moving for the first time,
# and when clicking on something that is already at the front.
if item_at_front == item:
angle_end += 360
this_item.ellipse_behaviour.set_angle_start(angle_start)
this_item.ellipse_behaviour.set_angle_end(angle_end)
if this_item == item:
if angle_start < angle_end:
angle_diff = angle_end - angle_start
else:
angle_diff = 360 - (angle_start - angle_end)
# print " debug: angle diff=%f" % angle_diff
# TODO: Set the number of frames, depending on the angle.
# otherwise the actor will take the same amount of time to reach
# the end angle regardless of how far it must move, causing it to
# move very slowly if it does not have far to move.
angle_end += angle_step
angle_start += angle_step
# Set the number of frames to be proportional to the distance to travel,
# so the speed is always the same
pos_to_move = 0
if pos_front < pos:
count = len(list_items)
pos_to_move = count + (pos - pos_front)
else:
pos_to_move = pos_front - pos
timeline_rotation.set_duration(int(angle_diff * .2))
# Remember what item will be at the fron when this timeline finishes
item_at_front = item
timeline_rotation.start()
def on_texture_button_press(actor, event, item):
# Ignore the events if the timeline_rotation is running (meaning, if the objects are moving)
# to simplify things
global timeline_rotation
if timeline_rotation and timeline_rotation.is_playing():
print "on_texture_button_press(): ignoring"
return False
else:
print "on_texture_button_press(): handling"
rotate_all_until_item_is_at_front(item)
return True
def main():
stage_color = clutter.Color(176, 176, 176, 255) # light gray
# Get the stage and set its size and color
stage = clutter.Stage()
stage.set_size(800, 600)
stage.set_color(stage_color)
# Create and add a label actor, hidden at first
global label_filename
label_filename = clutter.Text()
label_color = clutter.Color(96, 96, 144, 255) # blueish
label_filename.set_color(label_color)
label_filename.set_font_name("Sans 24")
label_filename.set_position(10, 10)
label_filename.set_opacity(0)
stage.add(label_filename)
label_filename.show()
# Add a plane under the ellipse of images
rect_color = clutter.Color(255, 255, 255, 255) # white
rect = clutter.Rectangle(rect_color)
rect.set_height(ELLIPSE_HEIGHT + 20)
rect.set_width(stage.get_width() + 100)
# Position it so that its center is under the images
rect.set_position(-(rect.get_width() - stage.get_width()) / 2,
ELLIPSE_Y + IMAGE_HEIGHT - (rect.get_height() / 2))
# Rotate it around its center
rect.set_rotation(clutter.X_AXIS, -90, 0, rect.get_height() / 2, 0)
stage.add(rect)
rect.show()
# show the stage
stage.connect("destroy", clutter.main_quit)
stage.show_all()
global timeline_rotation
timeline_rotation = clutter.Timeline(2000) # milliseconds
timeline_rotation.connect('completed', on_timeline_rotation_completed)
# Add an actor for each image
load_images("images")
add_image_actors(stage)
# timeline_rotation.set_loop(True)
# Move them a bit to start with
global list_items
if list_items:
rotate_all_until_item_is_at_front(list_items[0])
# Start the main loop, so we can respond to events
clutter.main()
return 0
if __name__ == '__main__':
sys.exit(main())