Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorflorent <florent.pigout@gmail.com>2011-04-28 09:34:59 (GMT)
committer florent <florent.pigout@gmail.com>2011-04-28 09:34:59 (GMT)
commit10376e6aa0eb8221ae61981a56df18dde0b84d5b (patch)
treea50da7ac1edca4191241ddceeae6a9f90c806287
parent05ac644c99c1ea7ea3b9ade7e6f799d955aec049 (diff)
manage graphics in a nicer way - lets see for dnd feature now\!
-rw-r--r--activity.py2
-rw-r--r--atoidejouer/ui/screens.py295
-rw-r--r--lib/__init__.py0
-rw-r--r--lib/euclid.py516
-rw-r--r--lib/graphics.py1681
-rw-r--r--lib/proximity.py88
-rw-r--r--lib/pytweener.py343
7 files changed, 2802 insertions, 123 deletions
diff --git a/activity.py b/activity.py
index 518b1e3..094dcab 100644
--- a/activity.py
+++ b/activity.py
@@ -147,7 +147,7 @@ class AToiDeJouerActivity(activity.Activity):
# add tool bars
self.set_toolbox(self._toolbox)
# ...
- _rate = 0.5
+ _rate = 0.25
# sequence dict and list
self.graphic_keys = StoryKeys('graphics', rate=_rate)
self.sound_keys = StoryKeys('sounds', rate=_rate)
diff --git a/atoidejouer/ui/screens.py b/atoidejouer/ui/screens.py
index f7ad2e5..044da17 100644
--- a/atoidejouer/ui/screens.py
+++ b/atoidejouer/ui/screens.py
@@ -9,6 +9,9 @@ from gettext import gettext as _
# gtk import
import cairo, gobject, gtk, glib
+# graphics import
+from lib import graphics
+
# sugar import
from sugar.activity import activity
from sugar.graphics.objectchooser import ObjectChooser
@@ -38,21 +41,39 @@ def _on_image_clicked(screen, widget, sequence_name):
screen.timeline.set_active_sequence(sequence_name)
-class ScreenStory(gtk.Frame): # gtk.Fixed
+def _on_drag_finish(image, scene, keys, size):
+ # _align = self._get_keys('graphics').set_align(sequence_name,
+ # frame, _current)
+ # DEBUG
+ logger.debug('[screens] _on_drag_finish - image.x: %s' % image.x)
+ logger.debug('[screens] _on_drag_finish - image.y: %s' % image.y)
+ # DEBUG
+ # parse size
+ _w , _h = size
+ # compute align
+ _x = float(image.x) / _w
+ _y = float(image.x) / _h
+ # DEBUG
+ logger.debug('[screens] _on_drag_finish - _x: %s' % _x)
+ logger.debug('[screens] _on_drag_finish - _y: %s' % _y)
+ # DEBUG
+ # TODO - compute new align
+
+
+class ScreenStory(graphics.Scene):
def __init__(self, toolbar, height_offset=0, width_offset=0,
set_canvas=False, frame=None):
- # init parent
- gtk.Frame.__init__(self)
- self.set_shadow_type(gtk.SHADOW_NONE)
+ # ..
+ graphics.Scene.__init__(self)
+ self.background_color = "#ffffff"
# keep toolbar
self.toolbar = toolbar
# ..
self._set_canvas = set_canvas
- # init frame dict
- self.__frame_dict = dict()
# init image dicts
self.__sizes = dict()
+ self.__graphics = dict()
self.__sounds = dict()
# init fullscreen flag
self.fullscreen = False
@@ -68,15 +89,6 @@ class ScreenStory(gtk.Frame): # gtk.Fixed
# and show
self._show()
- def __init_buffer(self):
- # ..
- _buffer = gtk.Fixed()
- # set has window for bg color issue
- _buffer.set_has_window(True)
- _buffer.modify_bg(gtk.STATE_NORMAL, COLOR_WHITE)
- # ..
- return _buffer
-
def _get_keys(self, type_):
# ..
if type_ == 'graphics':
@@ -100,11 +112,7 @@ class ScreenStory(gtk.Frame): # gtk.Fixed
def _check_clear(self):
# need clear
if self._get_keys('graphics').get_clear(self.toolbar.name) is True:
- # destroy all
- for _d_area in self.__frame_dict.values():
- _d_area.destroy()
- # ...
- self.__frame_dict = dict()
+ # ..
self.__sizes = dict()
# done
self._get_keys('graphics').set_clear(self.toolbar.name, False)
@@ -115,14 +123,42 @@ class ScreenStory(gtk.Frame): # gtk.Fixed
def refresh(self):
# get the current frame
- _frame = 0 if self.toolbar.activity._thread is None\
+ _time = 0.0 if self.toolbar.activity._thread is None\
else self.toolbar.activity._thread._time
+ # get the current frame
+ _rate = 1.0 if self.toolbar.activity._thread is None\
+ else self.toolbar.activity._thread._rate
# clear ??
- if self._check_clear() is True\
- or _frame not in self.__frame_dict:
- self.__buffer_update(_frame)
- elif _frame in self.__frame_dict:
- self.__set_child(self.__frame_dict[_frame])
+ self._check_clear()
+ # ..
+ self._refresh_screen_size()
+ # ..
+ _currents = list()
+ # for each images
+ for _n in self._get_keys('graphics').get_names():
+ # do move
+ _currents.append(self.__refresh_image(_n, _time, _rate))
+ # draw mask
+ _currents.append(self.__refresh_default('mask'))
+ # DEBUG
+ logger.debug('[screens] refresh - _currents: %s' % _currents)
+ # DEBUG
+ # ..
+ # self.queue_draw()
+ # TODO - hide previous
+ for _code, _image in self.__graphics.items():
+ if _code in _currents:
+ continue
+ else:
+ # DEBUG
+ logger.debug('[screens] refresh - not in _code: %s' % _code)
+ # DEBUG
+ _image.visible = False
+ self.queue_draw()
+ # show first
+ if self._fullscreen_changed is True:
+ # disable changed flag
+ self._fullscreen_changed = False
else:
pass
# refresh sounds if playing
@@ -132,7 +168,7 @@ class ScreenStory(gtk.Frame): # gtk.Fixed
# for each sound
for _n in self._get_keys('sounds').get_names():
# do move
- self.__refresh_sound(_n, _frame)
+ self.__refresh_sound(_n, _time)
else:
# stop all
for _code, _sound in self.__sounds.items():
@@ -141,64 +177,9 @@ class ScreenStory(gtk.Frame): # gtk.Fixed
else:
_sound.pause()
- def __set_child(self, child):
- # remove previous
- _previous = self.get_child()
- if _previous is None:
- pass
- else:
- self.remove(_previous)
- # do show
- child.show()
- # set new
- self.add(child)
-
- def __expose(self, drawing_area, event, frame):
- # ..
- if drawing_area.window is None:
- pass
- else:
- # ..
- _context = drawing_area.window.cairo_create()
- # init rec
- _rec = self.get_allocation()
- # paint it white
- _context.rectangle(0, 0, _rec.width, _rec.height)
- _context.set_source_rgb(1, 1, 1)
- _context.paint()
- # for each images
- for _n in self._get_keys('graphics').get_names():
- # do move
- self.__refresh_image(_n, frame, _context, _rec)
- # draw mask
- self.__refresh_default('mask', _context, _rec)
- # clean
- if frame in self.__frame_dict:
- del self.__frame_dict[frame]
- else:
- pass
- # update dict
- self.__frame_dict[frame] = drawing_area
- # show first
- if self._fullscreen_changed is True:
- # disable changed flag
- self._fullscreen_changed = False
- else:
- pass
-
- def __buffer_update(self, frame):
- # screen size first
- self._refresh_screen_size()
- #
- _drawing_area = gtk.DrawingArea()
- _drawing_area.connect("expose-event", self.__expose, frame)
- # ..
- self.__set_child(_drawing_area)
-
def _update_w_h(self, filename):
if filename in self.__sizes:
- # get image size
- return self.__sizes[filename]
+ pass
else:
# get graphics path
_path = storage.get_image_path(filename)
@@ -214,11 +195,14 @@ class ScreenStory(gtk.Frame): # gtk.Fixed
_new_w = _w
_new_h = _h
# update size
- self.__sizes[filename] = _new_w, _new_h
- # return new values
- return _new_w, _new_h
+ self.__sizes[filename] = _new_w, _new_h, _w, _h
+ # return new values
+ return self.__sizes[filename]
- def _update_x_y(self, width, height, x, y):
+ def _update_x_y(self, size, align):
+ # ..
+ _new_w, _new_h, _w, _h = size
+ _x, _y = align
#
if self.fullscreen is True\
or self._screen_width < 1024:
@@ -228,55 +212,122 @@ class ScreenStory(gtk.Frame): # gtk.Fixed
_scene_with = 1024
_scene_height = 516
# update x
- _x = ((self._screen_width - width) / 2)
- _x += int(x * _scene_with / 100.0)
+ _new_x = ((self._screen_width - _new_w) / 2)
+ _new_x += int(_x * _scene_with / 100.0)
# update y
- _y = ((self._screen_height - height - 88) / 2)
- _y += int(y * _scene_height / 100.0)
+ _new_y = ((self._screen_height - _new_h - 88) / 2)
+ _new_y += int(_y * _scene_height / 100.0)
# return it
- return _x, _y
+ return _new_x, _new_y
- def __refresh_default(self, default_name, context, rec):
- # _code = '__%s' % default_name
- _filename = '%s_default' % default_name
- # get/update width and height
- _w, _h = self._update_w_h(_filename)
- # update x, y
- _x, _y = self._update_x_y(_w, _h, 0, 0)
- # ..
- self.__update_drawing_area(context, rec, _filename,
- (_w, _h), (_x, _y))
+ def __refresh_default(self, default_name):
+ _code = '__%s' % default_name
+ # ensure current image
+ if _code in self.__graphics:
+ # ..
+ self.__graphics[_code].visible = True
+ else:
+ _filename = '%s_default' % default_name
+ # ..
+ _align = (0, 0)
+ # get/update width and height
+ _size = self._update_w_h(_filename)
+ # ..
+ self.__graphics[_code] =\
+ self.__update_drawing_area(_filename, _size, _align)
+ # and return displayed code
+ return _code
+
+ def __get_transition_align(self, sequence_name, filename, time, rate):
+ # get align
+ _x, _y = self._get_keys('graphics').get_align(sequence_name,
+ int(time), filename)
+ # get transition ratio
+ _ratio = time - int(time)
+ # DEBUG
+ logger.debug('[screens] __compute_transition_align - _ratio: %s' % _ratio)
+ # DEBUG
+ if _ratio < rate:
+ return _x, _y
+ else:
+ _next_x, _next_y = self._get_keys('graphics').get_align(sequence_name,
+ int(time) + 1, filename)
+ # ..
+ return _x + (_next_x - _x) * _ratio, _y + (_next_y - _y) * _ratio
- def __refresh_image(self, sequence_name, frame, context, rec):
+ def __refresh_image(self, sequence_name, time, rate):
# get sequence filename
_current, _f_type, _k_type = self._get_keys('graphics').get_current(
- sequence_name, frame)
+ sequence_name, int(time))
# need current
if _current is None:
- return
+ return None
else:
- # get align
- _p_align = self._get_keys('graphics').get_align(sequence_name,
- frame, _current)
- # get/update width and height
- _w, _h = self._update_w_h(_current)
+ pass
+ # get sequence file codes
+ _code = self._get_keys('graphics').get_code(
+ self.toolbar.name, sequence_name, _current)
+ # DEBUG
+ logger.debug('[screens] __refresh_image - _code: %s' % _code)
+ # DEBUG
+ # current file code
+ _code = key.random_str() if _code is None else _code
+ # get/update width and height
+ _size = self._update_w_h(_current)
+ # get align
+ _align = self.__get_transition_align(sequence_name, _current,
+ time, rate)
+ # ensure current image
+ if _code in self.__graphics:
+ # ..
+ _image = self.__graphics[_code]
# update x, y
- _x, _y = self._update_x_y(_w, _h, *_p_align)
+ _image.x, _image.y = self._update_x_y(_size, _align)
+ # get z_order
+ _image.z_order = self._get_keys('graphics').get_layout(
+ sequence_name)
+ # ..
+ _image.visible = True
+ else:
+ # ..
+ _image = self.__update_drawing_area(_current, _size, _align,
+ sequence_name=sequence_name, draggable=True)
# ..
- self.__update_drawing_area(context, rec, _current,
- (_w, _h), (_x, _y))
+ self.__graphics[_code] = _image
+ # ..
+ self._get_keys('graphics').set_code(self.toolbar.name, sequence_name,
+ _current, _code)
+ # and return displayed code
+ return _code
- def __update_drawing_area(self, context, rec, filename, size, align):
+ def __update_drawing_area(self, filename, size, align, sequence_name=None,
+ draggable=False):
# get path
_path = storage.get_image_path(filename)
- # split size
- _w, _h = size
- # init pixbuf
- _pixbuf = image.get_pixbuf(_path, _w, _h, use_max=True)
- # add source
- context.set_source_pixbuf(_pixbuf, *align)
- # paint context
- context.paint_with_alpha(1)
+ # update x, y
+ _x, _y = self._update_x_y(size, align)
+ # scale
+ _new_w, _new_h, _w, _h = size
+ # ..
+ _scale = float(_new_w)/_w
+ # _scale_y = float(_new_h)/_h
+ # get z_order
+ if sequence_name is None:
+ _z_order = 1000
+ else:
+ _z_order = self._get_keys('graphics').get_layout(sequence_name)
+ # image - TODO - draggable = True
+ _image = graphics.Image(_path, x=_x, y=_y, scale_x=_scale,
+ scale_y=_scale, z_order=_z_order, draggable=draggable)
+ # connect drag
+ if self._set_canvas is True:
+ pass
+ else:
+ _image.connect('on-drag-finish', _on_drag_finish, self, (_w, _h))
+ # do add
+ self.add_child(_image)
+ # ..
+ return _image
def __stop_unused_sounds(self, sequence_codes, current_code):
# hide previous
diff --git a/lib/__init__.py b/lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/__init__.py
diff --git a/lib/euclid.py b/lib/euclid.py
new file mode 100644
index 0000000..b3834d6
--- /dev/null
+++ b/lib/euclid.py
@@ -0,0 +1,516 @@
+#!/usr/bin/env python
+#
+# euclid graphics maths module
+#
+# Copyright (c) 2006 Alex Holkner
+# Alex.Holkner@mail.google.com
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 2.1 of the License, or (at your
+# option) any later version.
+#
+# This library is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+'''euclid graphics maths module
+
+Documentation and tests are included in the file "euclid.txt", or online
+at http://code.google.com/p/pyeuclid
+'''
+
+__docformat__ = 'restructuredtext'
+__version__ = '$Id$'
+__revision__ = '$Revision$'
+
+import math
+import operator
+import types
+
+
+
+class Vector2(object):
+ __slots__ = ['x', 'y']
+
+ def __init__(self, x=0, y=0):
+ self.x = x
+ self.y = y
+
+ def __copy__(self):
+ return self.__class__(self.x, self.y)
+
+ copy = __copy__
+
+ def __repr__(self):
+ return 'Vector2(%.2f, %.2f)' % (self.x, self.y)
+
+ def __eq__(self, other):
+ if not other: return False
+
+ if isinstance(other, Vector2):
+ return self.x == other.x and \
+ self.y == other.y
+ else:
+ if hasattr(other, '__len__') and len(other) == 2:
+ return self.x == other[0] and \
+ self.y == other[1]
+ else:
+ return False
+
+ def __neq__(self, other):
+ return not self.__eq__(other)
+
+ def __nonzero__(self):
+ return self.x != 0 or self.y != 0
+
+ def __len__(self):
+ return 2
+
+ def __getitem__(self, key):
+ return (self.x, self.y)[key]
+
+ def __setitem__(self, key, value):
+ l = [self.x, self.y]
+ l[key] = value
+ self.x, self.y = l
+
+ def __iter__(self):
+ return iter((self.x, self.y))
+
+ def __getattr__(self, name):
+ try:
+ return tuple([(self.x, self.y)['xy'.index(c)] \
+ for c in name])
+ except ValueError:
+ raise AttributeError, name
+
+ def __add__(self, other):
+ return Vector2(self.x + other.x, self.y + other.y)
+
+ __radd__ = __add__
+
+ def __iadd__(self, other):
+ self.x += other.x
+ self.y += other.y
+ return self
+
+ def __sub__(self, other):
+ return Vector2(self.x - other.x, self.y - other.y)
+
+ def __rsub__(self, other):
+ return Vector2(other.x - self.x, other.y - self.y)
+
+ def __mul__(self, other):
+ return Vector2(self.x * other, self.y * other)
+
+ __rmul__ = __mul__
+
+ def __imul__(self, other):
+ self.x *= other
+ self.y *= other
+ return self
+
+ def __div__(self, other):
+ return Vector2(operator.div(self.x, other),
+ operator.div(self.y, other))
+
+
+ def __rdiv__(self, other):
+ return Vector2(operator.div(other, self.x),
+ operator.div(other, self.y))
+
+ def __floordiv__(self, other):
+ return Vector2(operator.floordiv(self.x, other),
+ operator.floordiv(self.y, other))
+
+
+ def __rfloordiv__(self, other):
+ return Vector2(operator.floordiv(other, self.x),
+ operator.floordiv(other, self.y))
+
+ def __truediv__(self, other):
+ return Vector2(operator.truediv(self.x, other),
+ operator.truediv(self.y, other))
+
+ def __rtruediv__(self, other):
+ return Vector2(operator.truediv(other, self.x),
+ operator.truediv(other, self.y))
+
+ def __neg__(self):
+ return Vector2(-self.x, -self.y)
+
+ __pos__ = __copy__
+
+ def __abs__(self):
+ return math.sqrt(self.x * self.x + self.y * self.y)
+
+ magnitude = __abs__
+
+ def magnitude_squared(self):
+ return self.x * self.x + self.y * self.y
+
+ def normalize(self):
+ d = self.magnitude()
+ if d:
+ self.x /= d
+ self.y /= d
+ return self
+
+ def normalized(self):
+ d = self.magnitude()
+ if d:
+ return Vector2(self.x / d, self.y / d)
+ return self.copy()
+
+ def dot(self, other):
+ assert isinstance(other, Vector2)
+ return self.x * other.x + \
+ self.y * other.y
+
+ def cross(self):
+ return Vector2(self.y, -self.x)
+
+ def product(self, v2):
+ # product of our vector and the other vector's perpendicular
+ return self.x * v2.y - self.y * v2.x
+
+ def reflect(self, normal):
+ # assume normal is normalized
+ assert isinstance(normal, Vector2)
+ d = 2 * (self.x * normal.x + self.y * normal.y)
+ return Vector2(self.x - d * normal.x,
+ self.y - d * normal.y)
+
+ def limit(self, max_magnitude):
+ if self.magnitude() > max_magnitude:
+ self.normalize()
+ self *= max_magnitude
+
+ def heading(self):
+ return math.atan2(self.y, self.x)
+
+ def angle(self, other):
+ """angle between this and the other vector in radians"""
+ if self == -other: # same vector facing the opposite way will kill acos on float precision
+ return math.pi
+
+ return math.acos(self.normalized().dot(other.normalized()))
+
+
+# Geometry
+# Much maths thanks to Paul Bourke, http://astronomy.swin.edu.au/~pbourke
+# ---------------------------------------------------------------------------
+
+class Geometry(object):
+ def _connect_unimplemented(self, other):
+ raise AttributeError, 'Cannot connect %s to %s' % \
+ (self.__class__, other.__class__)
+
+ def _intersect_unimplemented(self, other):
+ raise AttributeError, 'Cannot intersect %s and %s' % \
+ (self.__class__, other.__class__)
+
+ _intersect_point2 = _intersect_unimplemented
+ _intersect_line2 = _intersect_unimplemented
+ _intersect_circle = _intersect_unimplemented
+ _connect_point2 = _connect_unimplemented
+ _connect_line2 = _connect_unimplemented
+ _connect_circle = _connect_unimplemented
+
+
+ def intersect(self, other):
+ raise NotImplementedError
+
+ def connect(self, other):
+ raise NotImplementedError
+
+ def distance(self, other):
+ c = self.connect(other)
+ if c:
+ return c.length
+ return 0.0
+
+def _intersect_point2_circle(P, C):
+ return (P - C.c).magnitude_squared() <= C.r * C.r
+
+def _intersect_line2_line2(A, B):
+ d = B.v.y * A.v.x - B.v.x * A.v.y
+ if d == 0:
+ return None
+
+ dy = A.p.y - B.p.y
+ dx = A.p.x - B.p.x
+ ua = (B.v.x * dy - B.v.y * dx) / d
+ if not A._u_in(ua):
+ return None
+ ub = (A.v.x * dy - A.v.y * dx) / d
+ if not B._u_in(ub):
+ return None
+
+ return Point2(A.p.x + ua * A.v.x,
+ A.p.y + ua * A.v.y)
+
+def _intersect_line2_circle(L, C):
+ a = L.v.magnitude_squared()
+ b = 2 * (L.v.x * (L.p.x - C.c.x) + \
+ L.v.y * (L.p.y - C.c.y))
+ c = C.c.magnitude_squared() + \
+ L.p.magnitude_squared() - \
+ 2 * C.c.dot(L.p) - \
+ C.r * C.r
+ det = b * b - 4 * a * c
+ if det < 0:
+ return None
+ sq = math.sqrt(det)
+ u1 = (-b + sq) / (2 * a)
+ u2 = (-b - sq) / (2 * a)
+ if not L._u_in(u1):
+ u1 = max(min(u1, 1.0), 0.0)
+ if not L._u_in(u2):
+ u2 = max(min(u2, 1.0), 0.0)
+
+ # Tangent
+ if u1 == u2:
+ return Point2(L.p.x + u1 * L.v.x,
+ L.p.y + u1 * L.v.y)
+
+ return LineSegment2(Point2(L.p.x + u1 * L.v.x,
+ L.p.y + u1 * L.v.y),
+ Point2(L.p.x + u2 * L.v.x,
+ L.p.y + u2 * L.v.y))
+
+def _connect_point2_line2(P, L):
+ d = L.v.magnitude_squared()
+ assert d != 0
+ u = ((P.x - L.p.x) * L.v.x + \
+ (P.y - L.p.y) * L.v.y) / d
+ if not L._u_in(u):
+ u = max(min(u, 1.0), 0.0)
+ return LineSegment2(P,
+ Point2(L.p.x + u * L.v.x,
+ L.p.y + u * L.v.y))
+
+def _connect_point2_circle(P, C):
+ v = P - C.c
+ v.normalize()
+ v *= C.r
+ return LineSegment2(P, Point2(C.c.x + v.x, C.c.y + v.y))
+
+def _connect_line2_line2(A, B):
+ d = B.v.y * A.v.x - B.v.x * A.v.y
+ if d == 0:
+ # Parallel, connect an endpoint with a line
+ if isinstance(B, Ray2) or isinstance(B, LineSegment2):
+ p1, p2 = _connect_point2_line2(B.p, A)
+ return p2, p1
+ # No endpoint (or endpoint is on A), possibly choose arbitrary point
+ # on line.
+ return _connect_point2_line2(A.p, B)
+
+ dy = A.p.y - B.p.y
+ dx = A.p.x - B.p.x
+ ua = (B.v.x * dy - B.v.y * dx) / d
+ if not A._u_in(ua):
+ ua = max(min(ua, 1.0), 0.0)
+ ub = (A.v.x * dy - A.v.y * dx) / d
+ if not B._u_in(ub):
+ ub = max(min(ub, 1.0), 0.0)
+
+ return LineSegment2(Point2(A.p.x + ua * A.v.x, A.p.y + ua * A.v.y),
+ Point2(B.p.x + ub * B.v.x, B.p.y + ub * B.v.y))
+
+def _connect_circle_line2(C, L):
+ d = L.v.magnitude_squared()
+ assert d != 0
+ u = ((C.c.x - L.p.x) * L.v.x + (C.c.y - L.p.y) * L.v.y) / d
+ if not L._u_in(u):
+ u = max(min(u, 1.0), 0.0)
+ point = Point2(L.p.x + u * L.v.x, L.p.y + u * L.v.y)
+ v = (point - C.c)
+ v.normalize()
+ v *= C.r
+ return LineSegment2(Point2(C.c.x + v.x, C.c.y + v.y), point)
+
+def _connect_circle_circle(A, B):
+ v = B.c - A.c
+ v.normalize()
+ return LineSegment2(Point2(A.c.x + v.x * A.r, A.c.y + v.y * A.r),
+ Point2(B.c.x - v.x * B.r, B.c.y - v.y * B.r))
+
+
+class Point2(Vector2, Geometry):
+ def __repr__(self):
+ return 'Point2(%.2f, %.2f)' % (self.x, self.y)
+
+ def intersect(self, other):
+ return other._intersect_point2(self)
+
+ def _intersect_circle(self, other):
+ return _intersect_point2_circle(self, other)
+
+ def connect(self, other):
+ return other._connect_point2(self)
+
+ def _connect_point2(self, other):
+ return LineSegment2(other, self)
+
+ def _connect_line2(self, other):
+ c = _connect_point2_line2(self, other)
+ if c:
+ return c._swap()
+
+ def _connect_circle(self, other):
+ c = _connect_point2_circle(self, other)
+ if c:
+ return c._swap()
+
+class Line2(Geometry):
+ __slots__ = ['p', 'v']
+
+ def __init__(self, *args):
+ if len(args) == 3:
+ assert isinstance(args[0], Point2) and \
+ isinstance(args[1], Vector2) and \
+ type(args[2]) == float
+ self.p = args[0].copy()
+ self.v = args[1] * args[2] / abs(args[1])
+ elif len(args) == 2:
+ if isinstance(args[0], Point2) and isinstance(args[1], Point2):
+ self.p = args[0].copy()
+ self.v = args[1] - args[0]
+ elif isinstance(args[0], Point2) and isinstance(args[1], Vector2):
+ self.p = args[0].copy()
+ self.v = args[1].copy()
+ else:
+ raise AttributeError, '%r' % (args,)
+ elif len(args) == 1:
+ if isinstance(args[0], Line2):
+ self.p = args[0].p.copy()
+ self.v = args[0].v.copy()
+ else:
+ raise AttributeError, '%r' % (args,)
+ else:
+ raise AttributeError, '%r' % (args,)
+
+ if not self.v:
+ raise AttributeError, 'Line has zero-length vector'
+
+ def __copy__(self):
+ return self.__class__(self.p, self.v)
+
+ copy = __copy__
+
+ def __repr__(self):
+ return 'Line2(<%.2f, %.2f> + u<%.2f, %.2f>)' % \
+ (self.p.x, self.p.y, self.v.x, self.v.y)
+
+ p1 = property(lambda self: self.p)
+ p2 = property(lambda self: Point2(self.p.x + self.v.x,
+ self.p.y + self.v.y))
+
+ def _apply_transform(self, t):
+ self.p = t * self.p
+ self.v = t * self.v
+
+ def _u_in(self, u):
+ return True
+
+ def intersect(self, other):
+ return other._intersect_line2(self)
+
+ def _intersect_line2(self, other):
+ return _intersect_line2_line2(self, other)
+
+ def _intersect_circle(self, other):
+ return _intersect_line2_circle(self, other)
+
+ def connect(self, other):
+ return other._connect_line2(self)
+
+ def _connect_point2(self, other):
+ return _connect_point2_line2(other, self)
+
+ def _connect_line2(self, other):
+ return _connect_line2_line2(other, self)
+
+ def _connect_circle(self, other):
+ return _connect_circle_line2(other, self)
+
+class Ray2(Line2):
+ def __repr__(self):
+ return 'Ray2(<%.2f, %.2f> + u<%.2f, %.2f>)' % \
+ (self.p.x, self.p.y, self.v.x, self.v.y)
+
+ def _u_in(self, u):
+ return u >= 0.0
+
+class LineSegment2(Line2):
+ def __repr__(self):
+ return 'LineSegment2(<%.2f, %.2f> to <%.2f, %.2f>)' % \
+ (self.p.x, self.p.y, self.p.x + self.v.x, self.p.y + self.v.y)
+
+ def _u_in(self, u):
+ return u >= 0.0 and u <= 1.0
+
+ def __abs__(self):
+ return abs(self.v)
+
+ def magnitude_squared(self):
+ return self.v.magnitude_squared()
+
+ def _swap(self):
+ # used by connect methods to switch order of points
+ self.p = self.p2
+ self.v *= -1
+ return self
+
+ length = property(lambda self: abs(self.v))
+
+class Circle(Geometry):
+ __slots__ = ['c', 'r']
+
+ def __init__(self, center, radius):
+ assert isinstance(center, Vector2) and type(radius) == float
+ self.c = center.copy()
+ self.r = radius
+
+ def __copy__(self):
+ return self.__class__(self.c, self.r)
+
+ copy = __copy__
+
+ def __repr__(self):
+ return 'Circle(<%.2f, %.2f>, radius=%.2f)' % \
+ (self.c.x, self.c.y, self.r)
+
+ def _apply_transform(self, t):
+ self.c = t * self.c
+
+ def intersect(self, other):
+ return other._intersect_circle(self)
+
+ def _intersect_point2(self, other):
+ return _intersect_point2_circle(other, self)
+
+ def _intersect_line2(self, other):
+ return _intersect_line2_circle(other, self)
+
+ def connect(self, other):
+ return other._connect_circle(self)
+
+ def _connect_point2(self, other):
+ return _connect_point2_circle(other, self)
+
+ def _connect_line2(self, other):
+ c = _connect_circle_line2(self, other)
+ if c:
+ return c._swap()
+
+ def _connect_circle(self, other):
+ return _connect_circle_circle(other, self)
diff --git a/lib/graphics.py b/lib/graphics.py
new file mode 100644
index 0000000..97704f7
--- /dev/null
+++ b/lib/graphics.py
@@ -0,0 +1,1681 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008-2010 Toms Bauģis <toms.baugis at gmail.com>
+# Dual licensed under the MIT or GPL Version 2 licenses.
+# See http://github.com/tbaugis/hamster_experiments/blob/master/README.textile
+
+import math
+import datetime as dt
+import gtk, gobject
+
+import pango, cairo
+import re
+
+try:
+ import pytweener
+except: # we can also live without tweener. Scene.animate will not work
+ pytweener = None
+
+import colorsys
+from collections import deque
+
+if cairo.version in ('1.8.2', '1.8.4'):
+ # in these two cairo versions the matrix multiplication was flipped
+ # http://bugs.freedesktop.org/show_bug.cgi?id=19221
+ def cairo_matrix_multiply(matrix1, matrix2):
+ return matrix2 * matrix1
+else:
+ def cairo_matrix_multiply(matrix1, matrix2):
+ return matrix1 * matrix2
+
+
+class Colors(object):
+ hex_color_normal = re.compile("#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})")
+ hex_color_short = re.compile("#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])")
+ hex_color_long = re.compile("#([a-fA-F0-9]{4})([a-fA-F0-9]{4})([a-fA-F0-9]{4})")
+
+ def parse(self, color):
+ assert color is not None
+
+ #parse color into rgb values
+ if isinstance(color, basestring):
+ match = self.hex_color_long.match(color)
+ if match:
+ color = [int(color, 16) / 65535.0 for color in match.groups()]
+ else:
+ match = self.hex_color_normal.match(color)
+ if match:
+ color = [int(color, 16) / 255.0 for color in match.groups()]
+ else:
+ match = self.hex_color_short.match(color)
+ color = [int(color + color, 16) / 255.0 for color in match.groups()]
+
+ elif isinstance(color, gtk.gdk.Color):
+ color = [color.red / 65535.0,
+ color.green / 65535.0,
+ color.blue / 65535.0]
+
+ else:
+ # otherwise we assume we have color components in 0..255 range
+ if color[0] > 1 or color[1] > 1 or color[2] > 1:
+ color = [c / 255.0 for c in color]
+
+ return color
+
+ def rgb(self, color):
+ return [c * 255 for c in self.parse(color)]
+
+ def gdk(self, color):
+ c = self.parse(color)
+ return gtk.gdk.Color(int(c[0] * 65535.0), int(c[1] * 65535.0), int(c[2] * 65535.0))
+
+ def is_light(self, color):
+ # tells you if color is dark or light, so you can up or down the
+ # scale for improved contrast
+ return colorsys.rgb_to_hls(*self.rgb(color))[1] > 150
+
+ def darker(self, color, step):
+ # returns color darker by step (where step is in range 0..255)
+ hls = colorsys.rgb_to_hls(*self.rgb(color))
+ return colorsys.hls_to_rgb(hls[0], hls[1] - step, hls[2])
+
+ def contrast(self, color, step):
+ """if color is dark, will return a lighter one, otherwise darker"""
+ hls = colorsys.rgb_to_hls(*self.rgb(color))
+ if self.is_light(color):
+ return colorsys.hls_to_rgb(hls[0], hls[1] - step, hls[2])
+ else:
+ return colorsys.hls_to_rgb(hls[0], hls[1] + step, hls[2])
+ # returns color darker by step (where step is in range 0..255)
+
+Colors = Colors() # this is a static class, so an instance will do
+
+
+class Graphics(object):
+ """If context is given upon contruction, will perform drawing
+ operations on context instantly. Otherwise queues up the drawing
+ instructions and performs them in passed-in order when _draw is called
+ with context.
+
+ Most of instructions are mapped to cairo functions by the same name.
+ Where there are differences, documenation is provided.
+
+ See http://cairographics.org/documentation/pycairo/2/reference/context.html
+ for detailed description of the cairo drawing functions.
+ """
+ def __init__(self, context = None):
+ self.context = context
+ self.colors = Colors # pointer to the color utilities instance
+ self.extents = None # bounds of the object, only if interactive
+ self.paths = None # paths for mouse hit checks
+ self._last_matrix = None
+ self.__new_instructions = [] # instruction set until it is converted into path-based instructions
+ self.__instruction_cache = []
+ self.cache_surface = None
+ self._cache_layout = None
+
+ def clear(self):
+ """clear all instructions"""
+ self.__new_instructions = []
+ self.__instruction_cache = []
+ self.paths = []
+
+ @staticmethod
+ def _stroke(context): context.stroke()
+ def stroke(self, color = None, alpha = 1):
+ if color or alpha < 1:self.set_color(color, alpha)
+ self._add_instruction(self._stroke,)
+
+ @staticmethod
+ def _fill(context): context.fill()
+ def fill(self, color = None, alpha = 1):
+ if color or alpha < 1:self.set_color(color, alpha)
+ self._add_instruction(self._fill,)
+
+ @staticmethod
+ def _mask(context, pattern): context.mask(pattern)
+ def mask(self, pattern):
+ self._add_instruction(self._mask, pattern)
+
+ @staticmethod
+ def _stroke_preserve(context): context.stroke_preserve()
+ def stroke_preserve(self, color = None, alpha = 1):
+ if color or alpha < 1:self.set_color(color, alpha)
+ self._add_instruction(self._stroke_preserve,)
+
+ @staticmethod
+ def _fill_preserve(context): context.fill_preserve()
+ def fill_preserve(self, color = None, alpha = 1):
+ if color or alpha < 1:self.set_color(color, alpha)
+ self._add_instruction(self._fill_preserve,)
+
+ @staticmethod
+ def _new_path(context): context.new_path()
+ def new_path(self):
+ self._add_instruction(self._new_path,)
+
+ @staticmethod
+ def _paint(context): context.paint()
+ def paint(self):
+ self._add_instruction(self._paint,)
+
+ @staticmethod
+ def _set_font_face(context, face): context.set_font_face(face)
+ def set_font_face(self, face):
+ self._add_instruction(self._set_font_face, face)
+
+ @staticmethod
+ def _set_font_size(context, size): context.set_font_size(size)
+ def set_font_size(self, size):
+ self._add_instruction(self._set_font_size, size)
+
+ @staticmethod
+ def _set_source(context, image):
+ context.set_source(image)
+ def set_source(self, image, x = 0, y = 0):
+ self._add_instruction(self._set_source, image)
+
+ @staticmethod
+ def _set_source_surface(context, surface, x, y):
+ context.set_source_surface(surface, x, y)
+ def set_source_surface(self, surface, x = 0, y = 0):
+ self._add_instruction(self._set_source_surface, surface, x, y)
+
+ @staticmethod
+ def _set_source_pixbuf(context, pixbuf, x, y):
+ context.set_source_pixbuf(pixbuf, x, y)
+ def set_source_pixbuf(self, pixbuf, x = 0, y = 0):
+ self._add_instruction(self._set_source_pixbuf, pixbuf, x, y)
+
+ @staticmethod
+ def _save_context(context): context.save()
+ def save_context(self):
+ self._add_instruction(self._save_context)
+
+ @staticmethod
+ def _restore_context(context): context.restore()
+ def restore_context(self):
+ self._add_instruction(self._restore_context)
+
+
+ @staticmethod
+ def _clip(context): context.clip()
+ def clip(self):
+ self._add_instruction(self._clip)
+
+ @staticmethod
+ def _translate(context, x, y): context.translate(x, y)
+ def translate(self, x, y):
+ self._add_instruction(self._translate, x, y)
+
+ @staticmethod
+ def _rotate(context, radians): context.rotate(radians)
+ def rotate(self, radians):
+ self._add_instruction(self._rotate, radians)
+
+ @staticmethod
+ def _move_to(context, x, y): context.move_to(x, y)
+ def move_to(self, x, y):
+ self._add_instruction(self._move_to, x, y)
+
+ @staticmethod
+ def _line_to(context, x, y): context.line_to(x, y)
+ def line_to(self, x, y = None):
+ if y is not None:
+ self._add_instruction(self._line_to, x, y)
+ elif isinstance(x, list) and y is None:
+ for x2, y2 in x:
+ self._add_instruction(self._line_to, x2, y2)
+
+
+ @staticmethod
+ def _rel_line_to(context, x, y): context.rel_line_to(x, y)
+ def rel_line_to(self, x, y = None):
+ if x is not None and y is not None:
+ self._add_instruction(self._rel_line_to, x, y)
+ elif isinstance(x, list) and y is None:
+ for x2, y2 in x:
+ self._add_instruction(self._rel_line_to, x2, y2)
+
+
+ @staticmethod
+ def _curve_to(context, x, y, x2, y2, x3, y3):
+ context.curve_to(x, y, x2, y2, x3, y3)
+ def curve_to(self, x, y, x2, y2, x3, y3):
+ """draw a curve. (x2, y2) is the middle point of the curve"""
+ self._add_instruction(self._curve_to, x, y, x2, y2, x3, y3)
+
+ @staticmethod
+ def _close_path(context): context.close_path()
+ def close_path(self):
+ self._add_instruction(self._close_path,)
+
+ @staticmethod
+ def _set_line_width(context, width):
+ context.set_line_width(width)
+ @staticmethod
+ def _set_dash(context, dash, dash_offset = 0):
+ context.set_dash(dash, dash_offset)
+
+ def set_line_style(self, width = None, dash = None, dash_offset = 0):
+ """change width and dash of a line"""
+ if width is not None:
+ self._add_instruction(self._set_line_width, width)
+
+ if dash is not None:
+ self._add_instruction(self._set_dash, dash, dash_offset)
+
+ def _set_color(self, context, r, g, b, a):
+ if a < 1:
+ context.set_source_rgba(r, g, b, a)
+ else:
+ context.set_source_rgb(r, g, b)
+
+ def set_color(self, color, alpha = 1):
+ """set active color. You can use hex colors like "#aaa", or you can use
+ normalized RGB tripplets (where every value is in range 0..1), or
+ you can do the same thing in range 0..65535.
+ also consider skipping this operation and specify the color on stroke and
+ fill.
+ """
+ color = self.colors.parse(color) # parse whatever we have there into a normalized triplet
+ if len(color) == 4 and alpha is None:
+ alpha = color[3]
+ r, g, b = color[:3]
+ self._add_instruction(self._set_color, r, g, b, alpha)
+
+ @staticmethod
+ def _arc(context, x, y, radius, start_angle, end_angle):
+ context.arc(x, y, radius, start_angle, end_angle)
+ def arc(self, x, y, radius, start_angle, end_angle):
+ """draw arc going counter-clockwise from start_angle to end_angle"""
+ self._add_instruction(self._arc, x, y, radius, start_angle, end_angle)
+
+ def circle(self, x, y, radius):
+ """draw circle"""
+ self._add_instruction(self._arc, x, y, radius, 0, math.pi * 2)
+
+ def ellipse(self, x, y, width, height, edges = None):
+ """draw 'perfect' ellipse, opposed to squashed circle. works also for
+ equilateral polygons"""
+ # the automatic edge case is somewhat arbitrary
+ steps = edges or max((32, width, height)) / 2
+
+ angle = 0
+ step = math.pi * 2 / steps
+ points = []
+ while angle < math.pi * 2:
+ points.append((width / 2.0 * math.cos(angle),
+ height / 2.0 * math.sin(angle)))
+ angle += step
+
+ min_x = min((point[0] for point in points))
+ min_y = min((point[1] for point in points))
+
+ self.move_to(points[0][0] - min_x + x, points[0][1] - min_y + y)
+ for p_x, p_y in points:
+ self.line_to(p_x - min_x + x, p_y - min_y + y)
+ self.line_to(points[0][0] - min_x + x, points[0][1] - min_y + y)
+
+
+ @staticmethod
+ def _arc_negative(context, x, y, radius, start_angle, end_angle):
+ context.arc_negative(x, y, radius, start_angle, end_angle)
+ def arc_negative(self, x, y, radius, start_angle, end_angle):
+ """draw arc going clockwise from start_angle to end_angle"""
+ self._add_instruction(self._arc_negative, x, y, radius, start_angle, end_angle)
+
+ @staticmethod
+ def _rounded_rectangle(context, x, y, x2, y2, corner_radius):
+ half_corner = corner_radius / 2
+
+ context.move_to(x + corner_radius, y)
+ context.line_to(x2 - corner_radius, y)
+ context.curve_to(x2 - half_corner, y, x2, y + half_corner, x2, y + corner_radius)
+ context.line_to(x2, y2 - corner_radius)
+ context.curve_to(x2, y2 - half_corner, x2 - half_corner, y2, x2 - corner_radius, y2)
+ context.line_to(x + corner_radius, y2)
+ context.curve_to(x + half_corner, y2, x, y2 - half_corner, x, y2 - corner_radius)
+ context.line_to(x, y + corner_radius)
+ context.curve_to(x, y + half_corner, x + half_corner, y, x + corner_radius, y)
+
+ @staticmethod
+ def _rectangle(context, x, y, w, h): context.rectangle(x, y, w, h)
+ def rectangle(self, x, y, width, height, corner_radius = 0):
+ "draw a rectangle. if corner_radius is specified, will draw rounded corners"
+ if corner_radius <= 0:
+ self._add_instruction(self._rectangle, x, y, width, height)
+ return
+
+ # make sure that w + h are larger than 2 * corner_radius
+ corner_radius = min(corner_radius, min(width, height) / 2)
+ x2, y2 = x + width, y + height
+ self._add_instruction(self._rounded_rectangle, x, y, x2, y2, corner_radius)
+
+ def fill_area(self, x, y, width, height, color, opacity = 1):
+ """fill rectangular area with specified color"""
+ self.rectangle(x, y, width, height)
+ self.fill(color, opacity)
+
+
+ def fill_stroke(self, fill = None, stroke = None, line_width = None):
+ """fill and stroke the drawn area in one go"""
+ if line_width: self.set_line_style(line_width)
+
+ if fill and stroke:
+ self.fill_preserve(fill)
+ elif fill:
+ self.fill(fill)
+
+ if stroke:
+ self.stroke(stroke)
+
+
+ @staticmethod
+ def _show_layout(context, layout, text, font_desc, alignment, width, wrap, ellipsize):
+ layout.set_font_description(font_desc)
+ layout.set_markup(text)
+ layout.set_width(int(width or -1))
+ layout.set_alignment(alignment)
+
+ if width > 0:
+ if wrap is not None:
+ layout.set_wrap(wrap)
+ else:
+ layout.set_ellipsize(ellipsize or pango.ELLIPSIZE_END)
+
+ context.show_layout(layout)
+
+ def create_layout(self, size = None):
+ """utility function to create layout with the default font. Size and
+ alignment parameters are shortcuts to according functions of the
+ pango.Layout"""
+ if not self.context:
+ # TODO - this is rather sloppy as far as exception goes
+ # should explain better
+ raise "Can not create layout without existing context!"
+
+ layout = self.context.create_layout()
+ font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+ if size: font_desc.set_size(size * pango.SCALE)
+
+ layout.set_font_description(font_desc)
+ return layout
+
+
+ def show_label(self, text, size = None, color = None):
+ """display text with system's default font"""
+ font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+ if color: self.set_color(color)
+ if size: font_desc.set_size(size * pango.SCALE)
+ self.show_layout(text, font_desc)
+
+
+ @staticmethod
+ def _show_text(context, text): context.show_text(text)
+ def show_text(self, text):
+ self._add_instruction(self._show_text, text)
+
+ @staticmethod
+ def _text_path(context, text): context.text_path(text)
+ def text_path(self, text):
+ """this function is most likely to change"""
+ self._add_instruction(self._text_path, text)
+
+ def show_layout(self, text, font_desc, alignment = pango.ALIGN_LEFT, width = -1, wrap = None, ellipsize = None):
+ """display text. font_desc is string of pango font description
+ often handier than calling this function directly, is to create
+ a class:Label object
+ """
+ layout = self._cache_layout = self._cache_layout or gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0))).create_layout()
+ self._add_instruction(self._show_layout, layout, text, font_desc, alignment, width, wrap, ellipsize)
+
+ def _add_instruction(self, function, *params):
+ if self.context:
+ function(self.context, *params)
+ else:
+ self.paths = None
+ self.__new_instructions.append((function, params))
+
+
+ def _draw(self, context, opacity):
+ """draw accumulated instructions in context"""
+
+ # if we have been moved around, we should update bounds
+ fresh_draw = self.__new_instructions and len(self.__new_instructions) > 0
+ if fresh_draw: #new stuff!
+ self.paths = []
+ self.__instruction_cache = self.__new_instructions
+ self.__new_instructions = []
+ else:
+ if not self.__instruction_cache:
+ return
+
+ for instruction, args in self.__instruction_cache:
+ if fresh_draw and instruction in (self._new_path, self._stroke, self._fill, self._clip):
+ self.paths.append(context.copy_path())
+
+ if opacity < 1 and instruction == self._set_color:
+ self._set_color(context, args[0], args[1], args[2], args[3] * opacity)
+ elif opacity < 1 and instruction == self._paint:
+ context.paint_with_alpha(opacity)
+ else:
+ instruction(context, *args)
+
+
+
+ def _draw_as_bitmap(self, context, opacity):
+ """
+ instead of caching paths, this function caches the whole drawn thing
+ use cache_as_bitmap on sprite to enable this mode
+ """
+ matrix = context.get_matrix()
+ matrix_changed = matrix != self._last_matrix
+ new_instructions = len(self.__new_instructions) > 0
+
+ if new_instructions or matrix_changed:
+ if new_instructions:
+ self.__instruction_cache = list(self.__new_instructions)
+ self.__new_instructions = deque()
+
+ self.paths = deque()
+ self.extents = None
+
+ if not self.__instruction_cache:
+ # no instructions - nothing to do
+ return
+
+ # instructions that end path
+ path_end_instructions = (self._new_path, self._clip, self._stroke, self._fill, self._stroke_preserve, self._fill_preserve)
+
+ # measure the path extents so we know the size of cache surface
+ # also to save some time use the context to paint for the first time
+ extents = gtk.gdk.Rectangle()
+ for instruction, args in self.__instruction_cache:
+ if instruction in path_end_instructions:
+ self.paths.append(context.copy_path())
+ exts = context.path_extents()
+ exts = gtk.gdk.Rectangle(int(exts[0]), int(exts[1]),
+ int(exts[2]-exts[0]), int(exts[3]-exts[1]))
+ if extents.width and extents.height:
+ extents = extents.union(exts)
+ else:
+ extents = exts
+
+
+ if instruction in (self._set_source_pixbuf, self._set_source_surface):
+ # draw a rectangle around the pathless instructions so that the extents are correct
+ pixbuf = args[0]
+ x = args[1] if len(args) > 1 else 0
+ y = args[2] if len(args) > 2 else 0
+ self._rectangle(context, x, y, pixbuf.get_width(), pixbuf.get_height())
+ self._clip()
+
+ if instruction == self._paint and opacity < 1:
+ context.paint_with_alpha(opacity)
+ elif instruction == self._set_color and opacity < 1:
+ self._set_color(context, args[0], args[1], args[2], args[3] * opacity)
+ else:
+ instruction(context, *args)
+
+
+ # avoid re-caching if we have just moved
+ just_transforms = new_instructions == False and \
+ matrix and self._last_matrix \
+ and all([matrix[i] == self._last_matrix[i] for i in range(4)])
+
+ # TODO - this does not look awfully safe
+ extents.x += matrix[4]
+ extents.y += matrix[5]
+ self.extents = extents
+
+ if not just_transforms:
+ # now draw the instructions on the caching surface
+ w = int(extents.width) + 1
+ h = int(extents.height) + 1
+ self.cache_surface = context.get_target().create_similar(cairo.CONTENT_COLOR_ALPHA, w, h)
+ ctx = gtk.gdk.CairoContext(cairo.Context(self.cache_surface))
+ ctx.translate(-extents.x, -extents.y)
+
+ ctx.transform(matrix)
+ for instruction, args in self.__instruction_cache:
+ instruction(ctx, *args)
+
+ self._last_matrix = matrix
+ else:
+ context.save()
+ context.identity_matrix()
+ context.translate(self.extents.x, self.extents.y)
+ context.set_source_surface(self.cache_surface)
+ if opacity < 1:
+ context.paint_with_alpha(opacity)
+ else:
+ context.paint()
+ context.restore()
+
+
+
+
+
+class Sprite(gtk.Object):
+ """The Sprite class is a basic display list building block: a display list
+ node that can display graphics and can also contain children.
+ Once you have created the sprite, use Scene's add_child to add it to
+ scene
+ """
+
+ __gsignals__ = {
+ "on-mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ "on-mouse-out": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ "on-mouse-down": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-mouse-up": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-drag-start": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-drag-finish": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-render": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
+ }
+
+ transformation_attrs = set(('x', 'y', 'rotation', 'scale_x', 'scale_y', 'pivot_x', 'pivot_y'))
+
+ visibility_attrs = set(('opacity', 'visible', 'z_order'))
+
+ cache_attrs = set(('_stroke_context', '_matrix', '_prev_parent_matrix', '_extents', '_scene'))
+
+ graphics_unrelated_attrs = set(('drag_x', 'drag_y', 'sprites', 'mouse_cursor', '_sprite_dirty'))
+
+
+
+ def __init__(self, x = 0, y = 0,
+ opacity = 1, visible = True,
+ rotation = 0, pivot_x = 0, pivot_y = 0,
+ scale_x = 1, scale_y = 1,
+ interactive = False, draggable = False,
+ z_order = 0, mouse_cursor = None,
+ cache_as_bitmap = False, snap_to_pixel = True):
+ gtk.Object.__init__(self)
+
+ self._scene = None
+
+ #: list of children sprites. Use :func:`add_child` to add sprites
+ self.sprites = []
+
+ #: instance of :ref:`graphics` for this sprite
+ self.graphics = Graphics()
+
+ #: boolean denoting whether the sprite responds to mouse events
+ self.interactive = interactive
+
+ #: boolean marking if sprite can be automatically dragged
+ self.draggable = draggable
+
+ #: relative x coordinate of the sprites' rotation point
+ self.pivot_x = pivot_x
+
+ #: relative y coordinates of the sprites' rotation point
+ self.pivot_y = pivot_y
+
+ #: sprite opacity
+ self.opacity = opacity
+
+ #: boolean visibility flag
+ self.visible = visible
+
+ #: pointer to parent :class:`Sprite` or :class:`Scene`
+ self.parent = None
+
+ #: sprite coordinates
+ self.x, self.y = x, y
+
+ #: rotation of the sprite in radians (use :func:`math.degrees` to convert to degrees if necessary)
+ self.rotation = rotation
+
+ #: scale X
+ self.scale_x = scale_x
+
+ #: scale Y
+ self.scale_y = scale_y
+
+ #: drawing order between siblings. The one with the highest z_order will be on top.
+ self.z_order = z_order
+
+ #: mouse-over cursor of the sprite. See :meth:`Scene.mouse_cursor`
+ #: for possible values
+ self.mouse_cursor = mouse_cursor
+
+ #: x position of the cursor within mouse upon drag. change this value
+ #: in on-drag-start to adjust drag point
+ self.drag_x = 0
+
+ #: y position of the cursor within mouse upon drag. change this value
+ #: in on-drag-start to adjust drag point
+ self.drag_y = 0
+
+ #: Whether the sprite should be cached as a bitmap. Default: true
+ #: Generally good when you have many static sprites
+ self.cache_as_bitmap = cache_as_bitmap
+
+ #: Should the sprite coordinates always rounded to full pixel. Default: true
+ #: Mostly this is good for performance but in some cases that can lead
+ #: to rounding errors in positioning.
+ self.snap_to_pixel = snap_to_pixel
+
+ self.__dict__["_sprite_dirty"] = True # flag that indicates that the graphics object of the sprite should be rendered
+ self.__dict__["_sprite_moved"] = True # flag that indicates that the graphics object of the sprite should be rendered
+
+ self._matrix = None
+ self._prev_parent_matrix = None
+
+ self._extents = None
+ self._prev_extents = None
+ self._stroke_context = None
+
+
+ def __setattr__(self, name, val):
+ if self.__dict__.get(name, "hamster_graphics_no_value_really") == val:
+ return
+ self.__dict__[name] = val
+
+ # prev parent matrix walks downwards
+ if name == '_prev_parent_matrix' and self.visible:
+ self._extents = None
+
+ # downwards recursive invalidation of parent matrix
+ for sprite in self.sprites:
+ sprite._prev_parent_matrix = None
+
+
+ if name in self.cache_attrs or name in self.graphics_unrelated_attrs:
+ return
+
+
+ """all the other changes influence cache vars"""
+
+ # either transforms or path operations - extents have to be recalculated
+ self._extents = None
+
+ if name == 'visible' and self.visible == False:
+ # when transforms happen while sprite is invisible
+ for sprite in self.sprites:
+ sprite._prev_parent_matrix = None
+
+
+ # on moves invalidate our matrix, child extent cache (as that depends on our transforms)
+ # as well as our parent's child extents as we moved
+ # then go into children and invalidate the parent matrix down the tree
+ if name in self.transformation_attrs:
+ self._matrix = None
+ for sprite in self.sprites:
+ sprite._prev_parent_matrix = None
+
+ # if attribute is not in transformation nor visibility, we conclude
+ # that it must be causing the sprite needs re-rendering
+ if name not in self.transformation_attrs and name not in self.visibility_attrs:
+ self.__dict__["_sprite_dirty"] = True
+
+ # on parent change invalidate the matrix
+ if name == 'parent':
+ self._prev_parent_matrix = None
+ return
+
+ if name == 'opacity' and self.__dict__.get("cache_as_bitmap") and hasattr(self, "graphics"):
+ # invalidating cache for the bitmap version as that paints opacity in the image
+ self.graphics._last_matrix = None
+
+ if name == 'z_order' and self.__dict__.get('parent'):
+ self.parent._sort()
+
+
+ self.redraw()
+
+
+ def _sort(self):
+ """sort sprites by z_order"""
+ self.sprites = sorted(self.sprites, key=lambda sprite:sprite.z_order)
+
+ def add_child(self, *sprites):
+ """Add child sprite. Child will be nested within parent"""
+ for sprite in sprites:
+ if sprite == self:
+ raise Exception("trying to add sprite to itself")
+ if sprite.parent:
+ sprite.x, sprite.y = self.from_scene_coords(*sprite.to_scene_coords())
+ sprite.parent.remove_child(sprite)
+
+ self.sprites.append(sprite)
+ sprite.parent = self
+ self._sort()
+
+
+ def remove_child(self, *sprites):
+ for sprite in sprites:
+ self.sprites.remove(sprite)
+ sprite._scene = None
+ sprite.parent = None
+
+ def bring_to_front(self):
+ """adjusts sprite's z-order so that the sprite is on top of it's
+ siblings"""
+ if not self.parent:
+ return
+ self.z_order = self.parent.sprites[-1].z_order + 1
+
+ def send_to_back(self):
+ """adjusts sprite's z-order so that the sprite is behind it's
+ siblings"""
+ if not self.parent:
+ return
+ self.z_order = self.parent.sprites[0].z_order - 1
+
+
+ def get_extents(self):
+ """measure the extents of the sprite's graphics. if context is provided
+ will use that to draw the paths"""
+ if self._extents:
+ return self._extents
+
+
+ if self._sprite_dirty:
+ # redrawing merely because we need fresh extents of the sprite
+ context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)))
+ context.transform(self.get_matrix())
+ self.emit("on-render")
+ self.__dict__["_sprite_dirty"] = False
+ self.graphics._draw(context, 1)
+
+
+ if not self.graphics.paths:
+ self.graphics._draw(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)), 1)
+
+ if not self.graphics.paths:
+ return None
+
+ context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)))
+ context.transform(self.get_matrix())
+
+ for path in self.graphics.paths:
+ context.append_path(path)
+ context.identity_matrix()
+
+ ext = context.path_extents()
+ ext = gtk.gdk.Rectangle(int(ext[0]), int(ext[1]),
+ int(ext[2] - ext[0]), int(ext[3] - ext[1]))
+
+ if not ext.width and not ext.height:
+ ext = None
+
+ self.__dict__['_extents'] = ext
+ self.__dict__['_stroke_context'] = context
+
+ return ext
+
+
+ def check_hit(self, x, y):
+ """check if the given coordinates are inside the sprite's fill or stroke
+ path"""
+
+ extents = self.get_extents()
+
+ if not extents:
+ return False
+
+ if extents.x <= x <= extents.x + extents.width and extents.y <= y <= extents.y + extents.height:
+ return self._stroke_context is None or self._stroke_context.in_fill(x, y)
+ else:
+ return False
+
+ def get_scene(self):
+ """returns class:`Scene` the sprite belongs to"""
+ if not self._scene:
+ if hasattr(self, 'parent') and self.parent:
+ if isinstance(self.parent, Sprite) == False:
+ scene = self.parent
+ else:
+ scene = self.parent.get_scene()
+
+ self._scene = scene
+
+ return self._scene
+
+ def redraw(self):
+ """queue redraw of the sprite. this function is called automatically
+ whenever a sprite attribute changes. sprite changes that happen
+ during scene redraw are ignored in order to avoid echoes.
+ Call scene.redraw() explicitly if you need to redraw in these cases.
+ """
+ scene = self.get_scene()
+ if scene and scene._redraw_in_progress == False and self.parent:
+ self.parent.redraw()
+
+ def animate(self, duration = None, easing = None, on_complete = None, on_update = None, **kwargs):
+ """Request paretn Scene to Interpolate attributes using the internal tweener.
+ Specify sprite's attributes that need changing.
+ `duration` defaults to 0.4 seconds and `easing` to cubic in-out
+ (for others see pytweener.Easing class).
+
+ Example::
+ # tween some_sprite to coordinates (50,100) using default duration and easing
+ self.animate(x = 50, y = 100)
+ """
+ scene = self.get_scene()
+ if scene:
+ scene.animate(self, duration, easing, on_complete, on_update, **kwargs)
+ else:
+ for key, val in kwargs.items():
+ setattr(self, key, val)
+
+ def get_local_matrix(self):
+ if not self._matrix:
+ self._matrix = cairo.Matrix()
+
+ if self.snap_to_pixel:
+ self._matrix.translate(int(self.x) + int(self.pivot_x), int(self.y) + int(self.pivot_y))
+ else:
+ self._matrix.translate(self.x + self.pivot_x, self.y + self.pivot_y)
+
+ if self.rotation:
+ self._matrix.rotate(self.rotation)
+
+
+ if self.snap_to_pixel:
+ self._matrix.translate(int(-self.pivot_x), int(-self.pivot_y))
+ else:
+ self._matrix.translate(-self.pivot_x, -self.pivot_y)
+
+
+ if self.scale_x != 1 or self.scale_y != 1:
+ self._matrix.scale(self.scale_x, self.scale_y)
+
+ return cairo.Matrix() * self._matrix
+
+
+ def get_matrix(self):
+ """return sprite's current transformation matrix"""
+ if self.parent:
+ return cairo_matrix_multiply(self.get_local_matrix(),
+ (self._prev_parent_matrix or self.parent.get_matrix()))
+ else:
+ return self.get_local_matrix()
+
+
+ def from_scene_coords(self, x=0, y=0):
+ """Converts x, y given in the scene coordinates to sprite's local ones
+ coordinates"""
+ matrix = self.get_matrix()
+ matrix.invert()
+ return matrix.transform_point(x, y)
+
+ def to_scene_coords(self, x=0, y=0):
+ """Converts x, y from sprite's local coordinates to scene coordinates"""
+ return self.get_matrix().transform_point(x, y)
+
+ def _draw(self, context, opacity = 1, parent_matrix = None):
+ if self.visible is False:
+ return
+
+ if (self._sprite_dirty): # send signal to redo the drawing when sprite is dirty
+ self.__dict__['_extents'] = None
+ self.emit("on-render")
+ self.__dict__["_sprite_dirty"] = False
+
+
+ parent_matrix = parent_matrix or cairo.Matrix()
+
+ # cache parent matrix
+ self._prev_parent_matrix = parent_matrix
+
+ matrix = self.get_local_matrix()
+
+ context.save()
+ context.transform(matrix)
+
+
+ if self.cache_as_bitmap:
+ self.graphics._draw_as_bitmap(context, self.opacity * opacity)
+ else:
+ self.graphics._draw(context, self.opacity * opacity)
+
+ self.__dict__['_prev_extents'] = self._extents or self.get_extents()
+
+ for sprite in self.sprites:
+ sprite._draw(context, self.opacity * opacity, cairo_matrix_multiply(matrix, parent_matrix))
+
+
+ context.restore()
+ context.new_path() #forget about us
+
+
+class BitmapSprite(Sprite):
+ """Caches given image data in a surface similar to targets, which ensures
+ that drawing it will be quick and low on CPU.
+ Image data can be either :class:`cairo.ImageSurface` or :class:`gtk.gdk.Pixbuf`
+ """
+ def __init__(self, image_data = None, cache_mode = None, **kwargs):
+ Sprite.__init__(self, **kwargs)
+
+ self.width, self.height = None, None
+ self.cache_mode = cache_mode or cairo.CONTENT_COLOR_ALPHA
+ #: image data
+ self.image_data = image_data
+
+ self._surface = None
+
+ self.cache_attrs = self.cache_attrs ^ set(('_surface',))
+
+ def __setattr__(self, name, val):
+ Sprite.__setattr__(self, name, val)
+ if name == 'image_data':
+ self.__dict__['_surface'] = None
+ if self.image_data:
+ self.__dict__['width'] = self.image_data.get_width()
+ self.__dict__['height'] = self.image_data.get_height()
+
+ def _draw(self, context, opacity = 1, parent_matrix = None):
+ if self.image_data is None or self.width is None or self.height is None:
+ return
+
+ if not self._surface:
+ # caching image on surface similar to the target
+ surface = context.get_target().create_similar(self.cache_mode,
+ self.width,
+ self.height)
+
+
+ local_context = gtk.gdk.CairoContext(cairo.Context(surface))
+ if isinstance(self.image_data, gtk.gdk.Pixbuf):
+ local_context.set_source_pixbuf(self.image_data, 0, 0)
+ else:
+ local_context.set_source_surface(self.image_data)
+ local_context.paint()
+
+ # add instructions with the resulting surface
+ self.graphics.clear()
+ self.graphics.rectangle(0, 0, self.width, self.height)
+ self.graphics.clip()
+ self.graphics.set_source_surface(surface)
+ self.graphics.paint()
+ self._surface = surface
+
+
+ Sprite._draw(self, context, opacity, parent_matrix)
+
+
+class Image(BitmapSprite):
+ """Displays image by path. Currently supports only PNG images."""
+ def __init__(self, path, **kwargs):
+ BitmapSprite.__init__(self, **kwargs)
+
+ #: path to the image
+ self.path = path
+
+ def __setattr__(self, name, val):
+ BitmapSprite.__setattr__(self, name, val)
+ if name == 'path': # load when the value is set to avoid penalty on render
+ self.image_data = cairo.ImageSurface.create_from_png(self.path)
+
+
+
+class Icon(BitmapSprite):
+ """Displays icon by name and size in the theme"""
+ def __init__(self, name, size=24, **kwargs):
+ BitmapSprite.__init__(self, **kwargs)
+ self.theme = gtk.icon_theme_get_default()
+
+ #: icon name from theme
+ self.name = name
+
+ #: icon size in pixels
+ self.size = size
+
+ def __setattr__(self, name, val):
+ BitmapSprite.__setattr__(self, name, val)
+ if name in ('name', 'size'): # no other reason to discard cache than just on path change
+ if self.__dict__.get('name') and self.__dict__.get('size'):
+ self.image_data = self.theme.load_icon(self.name, self.size, 0)
+ else:
+ self.image_data = None
+
+
+class Label(Sprite):
+ __gsignals__ = {
+ "on-change": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ }
+ def __init__(self, text = "", size = 10, color = None,
+ alignment = pango.ALIGN_LEFT,
+ max_width = None, wrap = None, ellipsize = None,
+ **kwargs):
+ Sprite.__init__(self, **kwargs)
+ self.width, self.height = None, None
+
+
+ self._test_context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A8, 0, 0)))
+ self._test_layout = self._test_context.create_layout()
+
+
+ #: pango.FontDescription, default is the system's font
+ self.font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+ self.font_desc.set_size(size * pango.SCALE)
+
+ #: color of label either as hex string or an (r,g,b) tuple
+ self.color = color
+
+ self._bounds_width = None
+
+ #: wrapping method. Can be set to pango. [WRAP_WORD, WRAP_CHAR,
+ #: WRAP_WORD_CHAR]
+ self.wrap = wrap
+
+ #: Ellipsize mode. Can be set to pango. [ELLIPSIZE_NONE,
+ #: ELLIPSIZE_START, ELLIPSIZE_MIDDLE, ELLIPSIZE_END]
+ self.ellipsize = ellipsize
+
+ #: alignment. one of pango.[ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER]
+ self.alignment = alignment
+
+ #: font size
+ self.size = size
+
+ #: maximum width of the label in pixels. if specified, the label
+ #: will be wrapped or ellipsized depending on the wrap and ellpisize settings
+ self.max_width = max_width
+
+ self.__surface = None
+
+ #: label text
+ self.text = text
+
+ self._measures = {}
+
+ self.connect("on-render", self.on_render)
+
+ self.cache_attrs = self.cache_attrs ^ set(("_letter_sizes", "__surface", "_ascent", "_bounds_width", "_measures"))
+
+
+ def __setattr__(self, name, val):
+ if self.__dict__.get(name, "hamster_graphics_no_value_really") != val:
+ if name == "width" and val and self.__dict__.get('_bounds_width') and val * pango.SCALE == self.__dict__['_bounds_width']:
+ return
+
+ Sprite.__setattr__(self, name, val)
+
+
+ if name == "width":
+ # setting width means consumer wants to contrain the label
+ if val is None or val == -1:
+ self.__dict__['_bounds_width'] = None
+ else:
+ self.__dict__['_bounds_width'] = val * pango.SCALE
+
+ if name in ("width", "text", "size", "font_desc", "wrap", "ellipsize", "max_width"):
+ self._measures = {}
+ # avoid chicken and egg
+ if hasattr(self, "text") and hasattr(self, "size"):
+ self.__dict__['width'], self.__dict__['height'] = self.measure(self.text)
+
+ if name == 'text':
+ self.emit('on-change')
+
+
+ def measure(self, text):
+ """measures given text with label's font and size.
+ returns width, height and ascent. Ascent's null in case if the label
+ does not have font face specified (and is thusly using pango)"""
+
+ if text in self._measures:
+ return self._measures[text]
+
+ width, height = None, None
+
+ context = self._test_context
+
+ layout = self._test_layout
+ layout.set_font_description(self.font_desc)
+ layout.set_markup(text)
+
+ max_width = 0
+ if self.max_width:
+ max_width = self.max_width * pango.SCALE
+
+ layout.set_width(int(self._bounds_width or max_width or -1))
+ layout.set_ellipsize(pango.ELLIPSIZE_NONE)
+
+ if self.wrap is not None:
+ layout.set_wrap(self.wrap)
+ else:
+ layout.set_ellipsize(self.ellipsize or pango.ELLIPSIZE_END)
+
+ width, height = layout.get_pixel_size()
+
+ self._measures[text] = width, height
+ return self._measures[text]
+
+
+ def on_render(self, sprite):
+ if not self.text:
+ self.graphics.clear()
+ return
+
+ self.graphics.set_color(self.color)
+
+ rect_width = self.width
+
+ max_width = 0
+ if self.max_width:
+ max_width = self.max_width * pango.SCALE
+
+ # when max width is specified and we are told to align in center
+ # do that (the pango instruction takes care of aligning within
+ # the lines of the text)
+ if self.alignment == pango.ALIGN_CENTER:
+ self.graphics.move_to(-(self.max_width - self.width)/2, 0)
+
+ bounds_width = max_width or self._bounds_width or -1
+
+ self.graphics.show_layout(self.text, self.font_desc,
+ self.alignment,
+ bounds_width,
+ self.wrap,
+ self.ellipsize)
+
+ if self._bounds_width:
+ rect_width = self._bounds_width / pango.SCALE
+
+ self.graphics.rectangle(0, 0, rect_width, self.height)
+ self.graphics.clip()
+
+
+
+class Rectangle(Sprite):
+ def __init__(self, w, h, corner_radius = 0, fill = None, stroke = None, line_width = 1, **kwargs):
+ Sprite.__init__(self, **kwargs)
+
+ #: width
+ self.width = w
+
+ #: height
+ self.height = h
+
+ #: fill color
+ self.fill = fill
+
+ #: stroke color
+ self.stroke = stroke
+
+ #: stroke line width
+ self.line_width = line_width
+
+ #: corner radius. Set bigger than 0 for rounded corners
+ self.corner_radius = corner_radius
+ self.connect("on-render", self.on_render)
+
+ def on_render(self, sprite):
+ self.graphics.set_line_style(width = self.line_width)
+ self.graphics.rectangle(0, 0, self.width, self.height, self.corner_radius)
+ self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
+
+class Polygon(Sprite):
+ def __init__(self, points, fill = None, stroke = None, line_width = 1, **kwargs):
+ Sprite.__init__(self, **kwargs)
+
+ #: list of (x,y) tuples that the line should go through. Polygon
+ #: will automatically close path.
+ self.points = points
+
+ #: fill color
+ self.fill = fill
+
+ #: stroke color
+ self.stroke = stroke
+
+ #: stroke line width
+ self.line_width = line_width
+
+ self.connect("on-render", self.on_render)
+
+ def on_render(self, sprite):
+ if not self.points: return
+
+ self.graphics.move_to(*self.points[0])
+ self.graphics.line_to(self.points)
+ self.graphics.close_path()
+
+ self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
+
+class Circle(Sprite):
+ def __init__(self, width, height, fill = None, stroke = None, line_width = 1, **kwargs):
+ Sprite.__init__(self, **kwargs)
+
+ #: circle width
+ self.width = width
+
+ #: circle height
+ self.height = height
+
+ #: fill color
+ self.fill = fill
+
+ #: stroke color
+ self.stroke = stroke
+
+ #: stroke line width
+ self.line_width = line_width
+
+ self.connect("on-render", self.on_render)
+
+ def on_render(self, sprite):
+ if self.width == self.height:
+ radius = self.width / 2.0
+ self.graphics.circle(radius, radius, radius)
+ else:
+ self.graphics.ellipse(0, 0, self.width, self.height)
+
+ self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
+
+class Scene(gtk.DrawingArea):
+ """ Drawing area for displaying sprites.
+ Add sprites to the Scene by calling :func:`add_child`.
+ Scene is descendant of `gtk.DrawingArea <http://www.pygtk.org/docs/pygtk/class-gtkdrawingarea.html>`_
+ and thus inherits all it's methods and everything.
+ """
+
+ __gsignals__ = {
+ "expose-event": "override",
+ "configure_event": "override",
+ "on-enter-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
+ "on-finish-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
+
+ "on-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+ "on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+ "on-drag-start": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+ "on-drag-finish": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+
+ "on-mouse-move": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-mouse-down": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-mouse-up": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-mouse-out": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+
+ "on-scroll": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ }
+
+ def __init__(self, interactive = True, framerate = 60,
+ background_color = None, scale = False, keep_aspect = True):
+ gtk.DrawingArea.__init__(self)
+ if interactive:
+ self.set_events(gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.ENTER_NOTIFY_MASK
+ | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.SCROLL_MASK
+ | gtk.gdk.KEY_PRESS_MASK)
+ self.connect("motion_notify_event", self.__on_mouse_move)
+ self.connect("enter_notify_event", self.__on_mouse_enter)
+ self.connect("leave_notify_event", self.__on_mouse_leave)
+ self.connect("button_press_event", self.__on_button_press)
+ self.connect("button_release_event", self.__on_button_release)
+ self.connect("scroll-event", self.__on_scroll)
+
+ #: list of sprites in scene. use :func:`add_child` to add sprites
+ self.sprites = []
+
+ #: framerate of animation. This will limit how often call for
+ #: redraw will be performed (that is - not more often than the framerate). It will
+ #: also influence the smoothness of tweeners.
+ self.framerate = framerate
+
+ #: Scene width. Will be `None` until first expose (that is until first
+ #: on-enter-frame signal below).
+ self.width = None
+
+ #: Scene height. Will be `None` until first expose (that is until first
+ #: on-enter-frame signal below).
+ self.height = None
+
+ #: instance of :class:`pytweener.Tweener` that is used by
+ #: :func:`animate` function, but can be also accessed directly for advanced control.
+ self.tweener = False
+ if pytweener:
+ self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.ease_in_out)
+
+ #: instance of :class:`Colors` class for color parsing
+ self.colors = Colors
+
+ #: read only info about current framerate (frames per second)
+ self.fps = 0 # inner frames per second counter
+
+ #: Last known x position of the mouse (set on expose event)
+ self.mouse_x = None
+
+ #: Last known y position of the mouse (set on expose event)
+ self.mouse_y = None
+
+ #: Background color of the scene. Use either a string with hex color or an RGB triplet.
+ self.background_color = background_color
+
+ #: Mouse cursor appearance.
+ #: Replace with your own cursor or set to False to have no cursor.
+ #: None will revert back the default behavior
+ self.mouse_cursor = None
+
+ blank_pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
+ self._blank_cursor = gtk.gdk.Cursor(blank_pixmap, blank_pixmap, gtk.gdk.Color(), gtk.gdk.Color(), 0, 0)
+
+
+ #: Miminum distance in pixels for a drag to occur
+ self.drag_distance = 1
+
+ self._last_frame_time = None
+ self._mouse_sprite = None
+ self._drag_sprite = None
+ self._mouse_down_sprite = None
+ self.__drag_started = False
+ self.__drag_start_x, self.__drag_start_y = None, None
+
+ self._mouse_in = False
+ self.__last_cursor = None
+
+ self.__drawing_queued = False
+ self._redraw_in_progress = False
+
+ #: When specified, upon window resize the content will be scaled
+ #: relative to original window size. Defaults to False.
+ self.scale = scale
+
+ #: Should the stage maintain aspect ratio upon scale if
+ #: :attr:`Scene.scale` is enabled. Defaults to true.
+ self.keep_aspect = keep_aspect
+
+ self._original_width, self._original_height = None, None
+
+
+
+ def add_child(self, *sprites):
+ """Add one or several :class:`Sprite` objects to the scene"""
+ for sprite in sprites:
+ if sprite == self:
+ raise Exception("trying to add sprite to itself")
+ if sprite.parent:
+ sprite.x, sprite.y = sprite.to_scene_coords(0, 0)
+ sprite.parent.remove_child(sprite)
+ self.sprites.append(sprite)
+ sprite.parent = self
+ self._sort()
+
+ def _sort(self):
+ """sort sprites by z_order"""
+ self.sprites = sorted(self.sprites, key=lambda sprite:sprite.z_order)
+
+
+ def remove_child(self, *sprites):
+ """Remove one or several :class:`Sprite` sprites from scene """
+ for sprite in sprites:
+ self.sprites.remove(sprite)
+ sprite._scene = None
+ sprite.parent = None
+
+ # these two mimic sprite functions so parent check can be avoided
+ def from_scene_coords(self, x, y): return x, y
+ def to_scene_coords(self, x, y): return x, y
+ def get_matrix(self): return cairo.Matrix()
+
+ def clear(self):
+ """Remove all sprites from scene"""
+ self.remove_child(*self.sprites)
+
+ def animate(self, sprite, duration = None, easing = None, on_complete = None, on_update = None, **kwargs):
+ """Interpolate attributes of the given object using the internal tweener
+ and redrawing scene after every tweener update.
+ Specify the sprite and sprite's attributes that need changing.
+ `duration` defaults to 0.4 seconds and `easing` to cubic in-out
+ (for others see pytweener.Easing class).
+
+ Redraw is requested right after creating the animation.
+ Example::
+
+ # tween some_sprite to coordinates (50,100) using default duration and easing
+ scene.animate(some_sprite, x = 50, y = 100)
+ """
+ if not self.tweener: # here we complain
+ raise Exception("pytweener was not found. Include it to enable animations")
+
+ tween = self.tweener.add_tween(sprite,
+ duration=duration,
+ easing=easing,
+ on_complete=on_complete,
+ on_update=on_update,
+ **kwargs)
+ self.redraw()
+ return tween
+
+
+ def redraw(self):
+ """Queue redraw. The redraw will be performed not more often than
+ the `framerate` allows"""
+ if self.__drawing_queued == False: #if we are moving, then there is a timeout somewhere already
+ self.__drawing_queued = True
+ self._last_frame_time = dt.datetime.now()
+ gobject.timeout_add(1000 / self.framerate, self.__redraw_loop)
+
+ def __redraw_loop(self):
+ """loop until there is nothing more to tween"""
+ self.queue_draw() # this will trigger do_expose_event when the current events have been flushed
+
+ self.__drawing_queued = self.tweener and self.tweener.has_tweens()
+ return self.__drawing_queued
+
+
+ def do_expose_event(self, event):
+ context = self.window.cairo_create()
+
+ # clip to the visible part
+ context.rectangle(event.area.x, event.area.y,
+ event.area.width, event.area.height)
+ if self.background_color:
+ color = self.colors.parse(self.background_color)
+ context.set_source_rgb(*color)
+ context.fill_preserve()
+ context.clip()
+
+ if self.scale:
+ aspect_x = self.width / self._original_width
+ aspect_y = self.height / self._original_height
+ if self.keep_aspect:
+ aspect_x = aspect_y = min(aspect_x, aspect_y)
+ context.scale(aspect_x, aspect_y)
+
+ self.mouse_x, self.mouse_y, mods = self.get_window().get_pointer()
+
+ self._redraw_in_progress = True
+
+ # update tweens
+ now = dt.datetime.now()
+ delta = (now - (self._last_frame_time or dt.datetime.now())).microseconds / 1000000.0
+ self._last_frame_time = now
+ if self.tweener:
+ self.tweener.update(delta)
+
+ self.fps = 1 / delta
+
+
+ # start drawing
+ self.emit("on-enter-frame", context)
+ for sprite in self.sprites:
+ sprite._draw(context)
+
+ self.__check_mouse(self.mouse_x, self.mouse_y)
+ self.emit("on-finish-frame", context)
+ self._redraw_in_progress = False
+
+
+ def do_configure_event(self, event):
+ if self._original_width is None:
+ self._original_width = float(event.width)
+ self._original_height = float(event.height)
+
+ self.width, self.height = event.width, event.height
+
+
+ def all_visible_sprites(self):
+ """Returns flat list of the sprite tree for simplified iteration"""
+ def all_recursive(sprites):
+ for sprite in sprites:
+ if sprite.visible:
+ yield sprite
+ if sprite.sprites:
+ for child in all_recursive(sprite.sprites):
+ yield child
+
+ return all_recursive(self.sprites)
+
+
+ def get_sprite_at_position(self, x, y):
+ """Returns the topmost visible interactive sprite for given coordinates"""
+ over = None
+
+ for sprite in self.all_visible_sprites():
+ if (sprite.interactive or sprite.draggable) and sprite.check_hit(x, y):
+ over = sprite
+
+ return over
+
+
+ def __check_mouse(self, x, y):
+ if x is None or self._mouse_in == False:
+ return
+
+ cursor = gtk.gdk.ARROW # default
+
+ if self.mouse_cursor is not None:
+ cursor = self.mouse_cursor
+
+ if self._drag_sprite:
+ cursor = self._drag_sprite.mouse_cursor or self.mouse_cursor or gtk.gdk.FLEUR
+ else:
+ #check if we have a mouse over
+ over = self.get_sprite_at_position(x, y)
+ if self._mouse_sprite and self._mouse_sprite != over:
+ self._mouse_sprite.emit("on-mouse-out")
+ self.emit("on-mouse-out", self._mouse_sprite)
+
+ if over:
+ if over.mouse_cursor is not None:
+ cursor = over.mouse_cursor
+
+ elif self.mouse_cursor is None:
+ # resort to defaults
+ if over.draggable:
+ cursor = gtk.gdk.FLEUR
+ else:
+ cursor = gtk.gdk.HAND2
+
+ if over != self._mouse_sprite:
+ over.emit("on-mouse-over")
+ self.emit("on-mouse-over", over)
+
+ self._mouse_sprite = over
+
+ if cursor == False:
+ cursor = self._blank_cursor
+
+ if not self.__last_cursor or cursor != self.__last_cursor:
+ if isinstance(cursor, gtk.gdk.Cursor):
+ self.window.set_cursor(cursor)
+ else:
+ self.window.set_cursor(gtk.gdk.Cursor(cursor))
+
+ self.__last_cursor = cursor
+
+
+ """ mouse events """
+ def __on_mouse_move(self, area, event):
+ state = event.state
+
+
+ if self._mouse_down_sprite and self._mouse_down_sprite.draggable \
+ and gtk.gdk.BUTTON1_MASK & event.state:
+ # dragging around
+ if not self.__drag_started:
+ drag_started = (self.__drag_start_x is not None and \
+ (self.__drag_start_x - event.x) ** 2 + \
+ (self.__drag_start_y - event.y) ** 2 > self.drag_distance ** 2)
+
+ if drag_started:
+ self._drag_sprite = self._mouse_down_sprite
+
+ self._drag_sprite.drag_x, self._drag_sprite.drag_y = self._drag_sprite.x, self._drag_sprite.y
+
+ self._drag_sprite.emit("on-drag-start", event)
+ self.emit("on-drag-start", self._drag_sprite, event)
+
+ self.__drag_started = True
+
+ if self.__drag_started:
+ diff_x, diff_y = event.x - self.__drag_start_x, event.y - self.__drag_start_y
+ if isinstance(self._drag_sprite.parent, Sprite):
+ matrix = self._drag_sprite.parent.get_matrix()
+ matrix.invert()
+ diff_x, diff_y = matrix.transform_distance(diff_x, diff_y)
+
+ self._drag_sprite.x, self._drag_sprite.y = self._drag_sprite.drag_x + diff_x, self._drag_sprite.drag_y + diff_y
+
+ self._drag_sprite.emit("on-drag", event)
+ self.emit("on-drag", self._drag_sprite, event)
+
+ else:
+ # avoid double mouse checks - the redraw will also check for mouse!
+ if not self.__drawing_queued:
+ self.__check_mouse(event.x, event.y)
+
+ self.emit("on-mouse-move", event)
+
+ def __on_mouse_enter(self, area, event):
+ self._mouse_in = True
+
+ def __on_mouse_leave(self, area, event):
+ self._mouse_in = False
+ if self._mouse_sprite:
+ self.emit("on-mouse-out", self._mouse_sprite)
+ self._mouse_sprite = None
+
+
+ def __on_button_press(self, area, event):
+ target = self.get_sprite_at_position(event.x, event.y)
+ self.__drag_start_x, self.__drag_start_y = event.x, event.y
+
+ self._mouse_down_sprite = target
+
+ if target:
+ target.emit("on-mouse-down", event)
+ self.emit("on-mouse-down", event)
+
+ def __on_button_release(self, area, event):
+ target = self.get_sprite_at_position(event.x, event.y)
+
+ if target:
+ target.emit("on-mouse-up", event)
+ self.emit("on-mouse-up", event)
+
+ # trying to not emit click and drag-finish at the same time
+ click = not self.__drag_started or (event.x - self.__drag_start_x) ** 2 + \
+ (event.y - self.__drag_start_y) ** 2 < self.drag_distance
+ if (click and self.__drag_started == False) or not self._drag_sprite:
+ if target:
+ target.emit("on-click", event)
+
+ self.emit("on-click", event, target)
+
+ if self._drag_sprite:
+ self._drag_sprite.emit("on-drag-finish", event)
+ self.emit("on-drag-finish", self._drag_sprite, event)
+
+ self._drag_sprite.drag_x, self._drag_sprite.drag_y = None, None
+ self._drag_sprite = None
+ self._mouse_down_sprite = None
+
+ self.__drag_started = False
+ self.__drag_start_x, self__drag_start_y = None, None
+
+ def __on_scroll(self, area, event):
+ self.emit("on-scroll", event)
diff --git a/lib/proximity.py b/lib/proximity.py
new file mode 100644
index 0000000..fc4d6a1
--- /dev/null
+++ b/lib/proximity.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+# - coding: utf-8 -
+# Copyright (C) 2010 Toms Bauģis <toms.baugis at gmail.com>
+
+"""
+ Proximity calculations
+"""
+
+from bisect import bisect
+
+class ProximityStore(object):
+ def __init__(self):
+ self.positions = {}
+ self.reverse_positions = {}
+
+ def update_position(position):
+ """Update position of the element"""
+ pass
+
+ def find_neighbours(location, radius):
+ pass
+
+
+# A AbstractProximityDatabase-style wrapper for the LQ bin lattice system
+class LQProximityStore(ProximityStore):
+ __slots__ = ['point1', 'point2', 'stride', 'grid_x', 'grid_y']
+ def __init__(self, point1, point2, stride):
+ ProximityStore.__init__(self)
+ self.point1, self.point2, self.stride = point1, point2, stride
+
+ # create the bin grid where we will be throwing in our friends
+ self.grid_x = range(point1.x, point2.x, stride)
+ self.grid_y = range(point1.y, point2.y, stride)
+
+ self.velocity_weight = 10
+
+
+ def update_position(self, boid):
+ bin = (bisect(self.grid_x, boid.location.x), bisect(self.grid_y, boid.location.y))
+ old_bin = self.reverse_positions.setdefault(boid, [])
+
+ #if bin has changed, move
+ if old_bin != bin:
+ if old_bin:
+ self.positions[old_bin].remove(boid)
+
+ self.positions.setdefault(bin, [])
+ self.positions[bin].append(boid)
+ self.reverse_positions[boid] = bin
+
+
+ def find_bins(self, boid, radius):
+ # TODO, would be neat to operate with vectors here
+ # create a bounding box and return all bins within it
+ velocity_weight = self.velocity_weight
+ min_x = bisect(self.grid_x, min(boid.location.x - radius,
+ boid.location.x + boid.velocity.x * velocity_weight - radius))
+ min_y = bisect(self.grid_y, min(boid.location.y - radius,
+ boid.location.y + boid.velocity.y * velocity_weight - radius))
+ max_x = bisect(self.grid_x, max(boid.location.x + radius,
+ boid.location.x + boid.velocity.x * velocity_weight + radius))
+ max_y = bisect(self.grid_y, max(boid.location.y + radius,
+ boid.location.y + boid.velocity.y * velocity_weight + radius))
+
+ bins = []
+ for x in range(min_x, max_x + 1):
+ for y in range(min_y, max_y + 1):
+ bins.append(self.positions.setdefault((x,y), []))
+ return bins
+
+
+ def find_neighbours(self, boid, radius):
+ bins = self.find_bins(boid, radius)
+
+ neighbours = []
+
+ for bin in bins:
+ for boid2 in bin:
+ if boid is boid2:
+ continue
+
+ dx = boid.location.x - boid2.location.x
+ dy = boid.location.y - boid2.location.y
+ d = dx * dx + dy * dy
+ if d < radius * radius:
+ neighbours.append((boid2, d))
+
+ return neighbours
diff --git a/lib/pytweener.py b/lib/pytweener.py
new file mode 100644
index 0000000..f5cacd7
--- /dev/null
+++ b/lib/pytweener.py
@@ -0,0 +1,343 @@
+# pyTweener
+#
+# Tweening functions for python
+#
+# Heavily based on caurina Tweener: http://code.google.com/p/tweener/
+#
+# Released under M.I.T License - see above url
+# Python version by Ben Harling 2009
+# All kinds of slashing and dashing by Toms Baugis 2010
+import math
+import collections
+import datetime as dt
+import time
+import re
+
+class Tweener(object):
+ def __init__(self, default_duration = None, tween = None):
+ """Tweener
+ This class manages all active tweens, and provides a factory for
+ creating and spawning tween motions."""
+ self.current_tweens = collections.defaultdict(set)
+ self.default_easing = tween or Easing.Cubic.ease_in_out
+ self.default_duration = default_duration or 1.0
+
+ def has_tweens(self):
+ return len(self.current_tweens) > 0
+
+
+ def add_tween(self, obj, duration = None, easing = None, on_complete = None, on_update = None, **kwargs):
+ """
+ Add tween for the object to go from current values to set ones.
+ Example: add_tween(sprite, x = 500, y = 200, duration = 0.4)
+ This will move the sprite to coordinates (500, 200) in 0.4 seconds.
+ For parameter "easing" you can use one of the pytweener.Easing
+ functions, or specify your own.
+ The tweener can handle numbers, dates and color strings in hex ("#ffffff").
+ This function performs overwrite style conflict solving - in case
+ if a previous tween operates on same attributes, the attributes in
+ question are removed from that tween.
+ """
+ if duration is None:
+ duration = self.default_duration
+
+ easing = easing or self.default_easing
+
+ tw = Tween(obj, duration, easing, on_complete, on_update, **kwargs )
+
+ if obj in self.current_tweens:
+ for current_tween in tuple(self.current_tweens[obj]):
+ prev_keys = set((key for (key, tweenable) in current_tween.tweenables))
+ dif = prev_keys & set(kwargs.keys())
+
+ for key, tweenable in tuple(current_tween.tweenables):
+ if key in dif:
+ current_tween.tweenables.remove((key, tweenable))
+
+ if not current_tween.tweenables:
+ current_tween.finish()
+ self.current_tweens[obj].remove(current_tween)
+
+
+ self.current_tweens[obj].add(tw)
+ return tw
+
+
+ def get_tweens(self, obj):
+ """Get a list of all tweens acting on the specified object
+ Useful for manipulating tweens on the fly"""
+ return self.current_tweens.get(obj, None)
+
+ def kill_tweens(self, obj = None):
+ """Stop tweening an object, without completing the motion or firing the
+ on_complete"""
+ if obj:
+ try:
+ del self.current_tweens[obj]
+ except:
+ pass
+ else:
+ self.current_tweens = collections.defaultdict(set)
+
+ def remove_tween(self, tween):
+ """"remove given tween without completing the motion or firing the on_complete"""
+ if tween.target in self.current_tweens and tween in self.current_tweens[tween.target]:
+ self.current_tweens[tween.target].remove(tween)
+ if not self.current_tweens[tween.target]:
+ del self.current_tweens[tween.target]
+
+ def finish(self):
+ """jump the the last frame of all tweens"""
+ for obj in self.current_tweens:
+ for tween in self.current_tweens[obj]:
+ tween.finish()
+ self.current_tweens = {}
+
+ def update(self, delta_seconds):
+ """update tweeners. delta_seconds is time in seconds since last frame"""
+
+ for obj in tuple(self.current_tweens):
+ for tween in tuple(self.current_tweens[obj]):
+ done = tween._update(delta_seconds)
+ if done:
+ self.current_tweens[obj].remove(tween)
+ if tween.on_complete: tween.on_complete(tween.target)
+
+ if not self.current_tweens[obj]:
+ del self.current_tweens[obj]
+
+ return self.current_tweens
+
+
+class Tween(object):
+ __slots__ = ('tweenables', 'target', 'delta', 'duration', 'ease', 'delta',
+ 'on_complete', 'on_update', 'complete')
+
+ def __init__(self, obj, duration, easing, on_complete, on_update, **kwargs):
+ """Tween object use Tweener.add_tween( ... ) to create"""
+ self.duration = duration
+ self.target = obj
+ self.ease = easing
+
+ # list of (property, start_value, delta)
+ self.tweenables = set()
+ for key, value in kwargs.items():
+ self.tweenables.add((key, Tweenable(self.target.__dict__[key], value)))
+
+ self.delta = 0
+ self.on_complete = on_complete
+ self.on_update = on_update
+ self.complete = False
+
+ def finish(self):
+ self._update(self.duration)
+
+ def _update(self, ptime):
+ """Update tween with the time since the last frame"""
+ self.delta = self.delta + ptime
+ if self.delta > self.duration:
+ self.delta = self.duration
+
+ if self.delta == self.duration:
+ for key, tweenable in self.tweenables:
+ self.target.__setattr__(key, tweenable.target_value)
+ else:
+ fraction = self.ease(self.delta / self.duration)
+
+ for key, tweenable in self.tweenables:
+ self.target.__setattr__(key, tweenable.update(fraction))
+
+ if self.delta == self.duration or len(self.tweenables) == 0:
+ self.complete = True
+
+ if self.on_update:
+ self.on_update(self.target)
+
+ return self.complete
+
+
+
+
+class Tweenable(object):
+ """a single attribute that has to be tweened from start to target"""
+ __slots__ = ('start_value', 'change', 'decode_func', 'target_value', 'update')
+
+ hex_color_normal = re.compile("#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})")
+ hex_color_short = re.compile("#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])")
+
+
+ def __init__(self, start_value, target_value):
+ self.decode_func = lambda x: x
+ self.target_value = target_value
+
+ def float_update(fraction):
+ return self.start_value + self.change * fraction
+
+ def date_update(fraction):
+ return dt.date.fromtimestamp(self.start_value + self.change * fraction)
+
+ def datetime_update(fraction):
+ return dt.datetime.fromtimestamp(self.start_value + self.change * fraction)
+
+ def color_update(fraction):
+ val = [max(min(self.start_value[i] + self.change[i] * fraction, 255), 0) for i in range(3)]
+ return "#%02x%02x%02x" % (val[0], val[1], val[2])
+
+
+ if isinstance(start_value, int) or isinstance(start_value, float):
+ self.start_value = start_value
+ self.change = target_value - start_value
+ self.update = float_update
+ else:
+ if isinstance(start_value, dt.datetime) or isinstance(start_value, dt.date):
+ if isinstance(start_value, dt.datetime):
+ self.update = datetime_update
+ else:
+ self.update = date_update
+
+ self.decode_func = lambda x: time.mktime(x.timetuple())
+ self.start_value = self.decode_func(start_value)
+ self.change = self.decode_func(target_value) - self.start_value
+
+ elif isinstance(start_value, basestring) \
+ and (self.hex_color_normal.match(start_value) or self.hex_color_short.match(start_value)):
+ self.update = color_update
+ if self.hex_color_normal.match(start_value):
+ self.decode_func = lambda val: [int(match, 16)
+ for match in self.hex_color_normal.match(val).groups()]
+
+ elif self.hex_color_short.match(start_value):
+ self.decode_func = lambda val: [int(match + match, 16)
+ for match in self.hex_color_short.match(val).groups()]
+
+ if self.hex_color_normal.match(target_value):
+ target_value = [int(match, 16)
+ for match in self.hex_color_normal.match(target_value).groups()]
+ else:
+ target_value = [int(match + match, 16)
+ for match in self.hex_color_short.match(target_value).groups()]
+
+ self.start_value = self.decode_func(start_value)
+ self.change = [target - start for start, target in zip(self.start_value, target_value)]
+
+
+
+"""Robert Penner's classes stripped from the repetetive c,b,d mish-mash
+(discovery of Patryk Zawadzki). This way we do the math once and apply to
+all the tweenables instead of repeating it for each attribute
+"""
+
+def inverse(method):
+ def real_inverse(t, *args, **kwargs):
+ t = 1 - t
+ return 1 - method(t, *args, **kwargs)
+ return real_inverse
+
+def symmetric(ease_in, ease_out):
+ def real_symmetric(t, *args, **kwargs):
+ if t < 0.5:
+ return ease_in(t * 2, *args, **kwargs) / 2
+
+ return ease_out((t - 0.5) * 2, *args, **kwargs) / 2 + 0.5
+ return real_symmetric
+
+class Symmetric(object):
+ def __init__(self, ease_in = None, ease_out = None):
+ self.ease_in = ease_in or inverse(ease_out)
+ self.ease_out = ease_out or inverse(ease_in)
+ self.ease_in_out = symmetric(self.ease_in, self.ease_out)
+
+
+class Easing(object):
+ """Class containing easing classes to use together with the tweener.
+ All of the classes have :func:`ease_in`, :func:`ease_out` and
+ :func:`ease_in_out` functions."""
+
+ Linear = Symmetric(lambda t: t, lambda t: t)
+ Quad = Symmetric(lambda t: t*t)
+ Cubic = Symmetric(lambda t: t*t*t)
+ Quart = Symmetric(lambda t: t*t*t*t)
+ Quint = Symmetric(lambda t: t*t*t*t*t)
+ Strong = Quint #oh i wonder why but the ported code is the same as in Quint
+
+ Circ = Symmetric(lambda t: 1 - math.sqrt(1 - t * t))
+ Sine = Symmetric(lambda t: 1 - math.cos(t * (math.pi / 2)))
+
+
+ def _back_in(t, s=1.70158):
+ return t * t * ((s + 1) * t - s)
+ Back = Symmetric(_back_in)
+
+
+ def _bounce_out(t):
+ if t < 1 / 2.75:
+ return 7.5625 * t * t
+ elif t < 2 / 2.75:
+ t = t - 1.5 / 2.75
+ return 7.5625 * t * t + 0.75
+ elif t < 2.5 / 2.75:
+ t = t - 2.25 / 2.75
+ return 7.5625 * t * t + .9375
+ else:
+ t = t - 2.625 / 2.75
+ return 7.5625 * t * t + 0.984375
+ Bounce = Symmetric(ease_out = _bounce_out)
+
+
+ def _elastic_in(t, springiness = 0, wave_length = 0):
+ if t in(0, 1):
+ return t
+
+ wave_length = wave_length or (1 - t) * 0.3
+
+ if springiness <= 1:
+ springiness = t
+ s = wave_length / 4
+ else:
+ s = wave_length / (2 * math.pi) * math.asin(t / springiness)
+
+ t = t - 1
+ return -(springiness * math.pow(2, 10 * t) * math.sin((t * t - s) * (2 * math.pi) / wave_length))
+ Elastic = Symmetric(_elastic_in)
+
+
+ def _expo_in(t):
+ if t in (0, 1): return t
+ return math.pow(2, 10 * t) * 0.001
+ Expo = Symmetric(_expo_in)
+
+
+
+class _Dummy(object):
+ def __init__(self, a, b, c):
+ self.a = a
+ self.b = b
+ self.c = c
+
+if __name__ == "__main__":
+ import datetime as dt
+
+ tweener = Tweener()
+ objects = []
+
+ for i in range(10000):
+ objects.append(_Dummy(dt.datetime.now(), i-100, i-100))
+
+
+ total = dt.datetime.now()
+
+ t = dt.datetime.now()
+ print "Adding 10000 objects..."
+ for i, o in enumerate(objects):
+ tweener.add_tween(o, a = dt.datetime.now() - dt.timedelta(days=3),
+ b = i,
+ c = i,
+ duration = 1.0,
+ easing=Easing.Circ.ease_in_out)
+ print dt.datetime.now() - t
+
+ t = dt.datetime.now()
+ print "Updating 10 times......"
+ for i in range(11): #update 1000 times
+ tweener.update(0.1)
+ print dt.datetime.now() - t