[billreminder] Updating graphing lib.
- From: Og B. Maciel <ogmaciel src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [billreminder] Updating graphing lib.
- Date: Fri, 27 Nov 2009 13:46:21 +0000 (UTC)
commit af1dbadc8fbcc6fe635aa0b8b2c6a089d443741d
Author: Og B. Maciel <ogmaciel gnome org>
Date: Fri Nov 27 08:46:06 2009 -0500
Updating graphing lib.
src/gui/widgets/charting.py | 104 ++++++------
src/gui/widgets/graphics.py | 394 +++++++++++++++++++-----------------------
src/gui/widgets/pytweener.py | 5 +
3 files changed, 234 insertions(+), 269 deletions(-)
---
diff --git a/src/gui/widgets/charting.py b/src/gui/widgets/charting.py
index d9c8306..4385600 100644
--- a/src/gui/widgets/charting.py
+++ b/src/gui/widgets/charting.py
@@ -34,7 +34,6 @@ http://projecthamster.wordpress.com/
"""
import gtk
-import gobject
import cairo, pango
import copy
import math
@@ -44,7 +43,7 @@ import time
import colorsys
import logging
-import graphics, pytweener
+import graphics
def size_list(set, target_set):
@@ -100,7 +99,6 @@ class Chart(graphics.Area):
self.background = Tripplet-tuple of background color in RGB
self.chart_background = Tripplet-tuple of chart background color in RGB
self.bar_base_color = Tripplet-tuple of bar color in RGB
- self.bars_beveled = Should bars be beveled.
self.show_scale = Should we show scale values. See grid_stride!
self.grid_stride = Step of grid. If expressed in normalized range
@@ -123,14 +121,13 @@ class Chart(graphics.Area):
# options
self.max_bar_width = args.get("max_bar_width", 500)
self.legend_width = args.get("legend_width", 0)
- self.animate = args.get("animate", True)
+ self.animation = args.get("animate", True)
self.background = args.get("background", None)
self.chart_background = args.get("chart_background", None)
self.bar_base_color = args.get("bar_base_color", None)
self.grid_stride = args.get("grid_stride", None)
- self.bars_beveled = args.get("bars_beveled", False)
self.values_on_bars = args.get("values_on_bars", False)
self.value_format = args.get("value_format", "%s")
self.show_scale = args.get("show_scale", False)
@@ -140,11 +137,17 @@ class Chart(graphics.Area):
self.framerate = args.get("framerate", 60)
# other stuff
- self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.easeInOut)
- self.last_frame_time = None
- self.moving = False
-
self.bars = []
+ self.keys = []
+ self.stack_keys = []
+
+ self.key_colors = {} # key:color dictionary. if key's missing will grab basecolor
+ self.stack_key_colors = {} # key:color dictionary. if key's missing will grab basecolor
+
+
+ # use these to mark area where the "real" drawing is going on
+ self.graph_x, self.graph_y = 0, 0
+ self.graph_width, self.graph_height = None, None
def get_bar_color(self, index):
@@ -182,34 +185,13 @@ class Chart(graphics.Area):
self._update_targets()
- if self.animate:
- self.last_frame_time = dt.datetime.now()
- if not self.moving: #if we are moving, then there is a timeout somewhere already
- gobject.timeout_add(1000 / self.framerate, self._interpolate)
- else:
- self.tweener.update(self.tweener.defaultDuration) # set to end frame
-
- self.redraw_canvas()
-
-
- def _interpolate(self):
- """Internal function to do the math, going from previous set to the
- new one, and redraw graph"""
- #this can get called before expose
- self.moving = self.tweener.hasTweens()
-
- if not self.window:
- self.redraw_canvas()
- return False
+ if not self.animation:
+ self.tweener.finish()
- time_since_start = (dt.datetime.now() - self.last_frame_time).microseconds / 1000000.0
- self.tweener.update(time_since_start)
self.redraw_canvas()
- self.last_frame_time = dt.datetime.now()
- return self.moving
- def _render(self):
+ def on_expose(self):
# fill whole area
if self.background:
self.fill_area(0, 0, self.width, self.height, self.background)
@@ -238,15 +220,26 @@ class Chart(graphics.Area):
return bars
retarget(self.bars, self.data)
+
+
+ def longest_label(self, labels):
+ """returns width of the longest label"""
+ max_extent = 0
+ for label in labels:
+ self.layout.set_text(label)
+ label_w, label_h = self.layout.get_pixel_size()
+ max_extent = max(label_w + 5, max_extent)
+
+ return max_extent
def draw(self):
logging.error("OMG OMG, not implemented!!!")
class BarChart(Chart):
- def _render(self):
+ def on_expose(self):
context = self.context
- Chart._render(self)
+ Chart.on_expose(self)
# determine graph dimensions
if self.show_stack_labels:
@@ -284,9 +277,6 @@ class BarChart(Chart):
self.max_bar_width)
gap = bar_width * 0.05
- # flip hamster.graphics matrix so we don't think upside down
- self.set_value_range(y_max = 0, y_min = self.graph_height)
-
# bars and keys
max_bar_size = self.graph_height
#make sure bars don't hit the ceiling
@@ -305,7 +295,7 @@ class BarChart(Chart):
intended_x = (bar_width * i) + (bar_width - label_w) / 2.0
if not prev_label_end or intended_x > prev_label_end:
- self.move_to(intended_x, -4)
+ self.context.move_to(intended_x, self.graph_height - 4)
context.show_layout(self.layout)
prev_label_end = intended_x + label_w + 3
@@ -321,20 +311,24 @@ class BarChart(Chart):
bar_size = round(max_bar_size * bar.size)
bar_start += bar_size
+ last_color = self.stack_key_colors.get(self.stack_keys[j],
+ self.get_bar_color(j))
self.draw_bar(bar_x,
self.graph_height - bar_start,
round(bar_width - (gap * 2)),
bar_size,
- self.get_bar_color(j))
+ last_color)
else:
bar_size = round(max_bar_size * self.bars[i].size)
bar_start = bar_size
+ last_color = self.key_colors.get(self.keys[i],
+ base_color)
self.draw_bar(bar_x,
self.graph_y + self.graph_height - bar_size,
round(bar_width - (gap * 2)),
bar_size,
- base_color)
+ last_color)
if self.values_on_bars: # it's either stack labels or values at the end for now
@@ -354,6 +348,13 @@ class BarChart(Chart):
label_y = self.graph_y + self.graph_height - bar_start - label_h + 5
context.move_to(self.graph_x + (bar_width * i) + (bar_width - label_w) / 2.0, label_y)
+
+ # we are in the bar so make sure that the font color is distinguishable
+ if colorsys.rgb_to_hls(*graphics.Colors.rgb(last_color))[1] < 150:
+ self.set_color(graphics.Colors.almost_white)
+ else:
+ self.set_color(graphics.Colors.aluminium[5])
+
context.show_layout(self.layout)
# values on bars
@@ -462,9 +463,9 @@ class BarChart(Chart):
class HorizontalBarChart(Chart):
- def _render(self):
+ def on_expose(self):
context = self.context
- Chart._render(self)
+ Chart.on_expose(self)
rowcount, keys = len(self.keys), self.keys
# push graph to the right, so it doesn't overlap
@@ -530,7 +531,8 @@ class HorizontalBarChart(Chart):
bar_size = round(max_bar_size * bar.size)
bar_height = round(bar_width - (gap * 2))
- last_color = self.get_bar_color(j)
+ last_color = self.stack_key_colors.get(self.stack_keys[j],
+ self.get_bar_color(j))
self.draw_bar(self.graph_x + bar_start,
bar_y,
bar_size,
@@ -542,8 +544,12 @@ class HorizontalBarChart(Chart):
bar_start = bar_size
bar_height = round(bar_width - (gap * 2))
+
+ last_color = self.key_colors.get(self.keys[i],
+ base_color)
+
self.draw_bar(self.graph_x, bar_y, bar_size, bar_height,
- base_color)
+ last_color)
# values on bars
if self.stack_keys:
@@ -561,9 +567,7 @@ class HorizontalBarChart(Chart):
self.set_color(graphics.Colors.aluminium[5])
else:
# we are in the bar so make sure that the font color is distinguishable
- # this is a hamster fix
- # TODO - drop the library bit, we will never be adopted
- if colorsys.rgb_to_hls(*last_color)[1] < 150:
+ if colorsys.rgb_to_hls(*graphics.Colors.rgb(last_color))[1] < 150:
self.set_color(graphics.Colors.almost_white)
else:
self.set_color(graphics.Colors.aluminium[5])
@@ -587,9 +591,9 @@ class HorizontalDayChart(Chart):
self.show()
self.redraw_canvas()
- def _render(self):
+ def on_expose(self):
context = self.context
- Chart._render(self)
+ Chart.on_expose(self)
rowcount, keys = len(self.keys), self.keys
start_hour = 0
diff --git a/src/gui/widgets/graphics.py b/src/gui/widgets/graphics.py
index b308b95..c7a02a5 100644
--- a/src/gui/widgets/graphics.py
+++ b/src/gui/widgets/graphics.py
@@ -3,11 +3,28 @@ import gtk, gobject
import pango, cairo
+import pytweener
+from pytweener import Easing
+
class Colors(object):
aluminium = [(238, 238, 236), (211, 215, 207), (186, 189, 182),
(136, 138, 133), (85, 87, 83), (46, 52, 54)]
almost_white = (250, 250, 250)
+ @staticmethod
+ def normalize_rgb(color):
+ # turns your average rgb into values with components in range 0..1
+ # if none of the componets are over 1 - will return what it got
+ if color[0] > 1 or color[1] > 0 or color[2] > 0:
+ color = [c / 255.0 for c in color]
+ return color
+
+ @staticmethod
+ def rgb(color):
+ #return color that has each component in 0..255 range
+ return [c*255 for c in Colors.normalize_rgb(color)]
+
+
class Area(gtk.DrawingArea):
"""Abstraction on top of DrawingArea to work specifically with cairo"""
__gsignals__ = {
@@ -17,170 +34,70 @@ class Area(gtk.DrawingArea):
"button-release": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
}
- def do_configure_event ( self, event ):
- (self.__width, self.__height) = self.window.get_size()
- self.queue_draw()
-
- def do_expose_event ( self, event ):
- self.width, self.height = self.window.get_size()
- self.context = self.window.cairo_create()
-
-
- self.context.set_antialias(cairo.ANTIALIAS_NONE)
- self.context.rectangle(event.area.x, event.area.y,
- event.area.width, event.area.height)
- self.context.clip()
-
- self.layout = self.context.create_layout()
- default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
- default_font.set_size(self.font_size * pango.SCALE)
- self.layout.set_font_description(default_font)
- alloc = self.get_allocation() #x, y, width, height
- self.width, self.height = alloc.width, alloc.height
-
- self.mouse_regions = [] #reset since these can move in each redraw
- self._render()
-
def __init__(self):
gtk.DrawingArea.__init__(self)
self.set_events(gtk.gdk.EXPOSURE_MASK
- | gtk.gdk.LEAVE_NOTIFY_MASK
- | gtk.gdk.BUTTON_PRESS_MASK
- | gtk.gdk.BUTTON_RELEASE_MASK
- | gtk.gdk.POINTER_MOTION_MASK
- | gtk.gdk.POINTER_MOTION_HINT_MASK)
+ | gtk.gdk.LEAVE_NOTIFY_MASK
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.POINTER_MOTION_HINT_MASK)
self.connect("button_release_event", self.__on_button_release)
self.connect("motion_notify_event", self.__on_mouse_move)
self.connect("leave_notify_event", self.__on_mouse_out)
-
- self.context = None
- self.layout = None
- self.width = None
- self.height = None
- self.value_boundaries = None #x_min, x_max, y_min, y_max
-
- self.x_factor, self.y_factor = None, None
-
self.font_size = 8
-
- # use these to mark area where the "real" drawing is going on
- self.graph_x, self.graph_y = 0, 0
- self.graph_width, self.graph_height = None, None
-
self.mouse_regions = [] #regions of drawing that respond to hovering/clicking
- self.__prev_mouse_regions = None
- def set_text(self, text):
- # sets text and returns width and height of the layout
- self.layout.set_text(text)
- w, h = self.layout.get_pixel_size()
- return w, h
-
+ self.context, self.layout = None, None
+ self.width, self.height = None, None
+ self.__prev_mouse_regions = None
- def set_color(self, color, opacity = None):
- if color[0] > 1 or color[1] > 0 or color[2] > 0:
- color = [c / 255.0 for c in color]
+ self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.easeInOut)
+ self.framerate = 30 #thirty seems to be good enough to avoid flicker
+ self.last_frame_time = None
+ self.__animating = False
+
+ def on_expose(self):
+ """ on_expose event is where you hook in all your drawing
+ canvas has been initialized for you """
+ raise NotImplementedError
- if opacity:
- self.context.set_source_rgba(color[0], color[1], color[2], opacity)
- elif len(color) == 3:
- self.context.set_source_rgb(*color)
- else:
- self.context.set_source_rgba(*color)
+ def redraw_canvas(self):
+ """Redraw canvas. Triggers also to do all animations"""
+ if not self.__animating: #if we are moving, then there is a timeout somewhere already
+ self.__animating = True
+ self.last_frame_time = dt.datetime.now()
+ gobject.timeout_add(1000 / self.framerate, self.__interpolate)
+
+ """ animation bits """
+ def __interpolate(self):
+ self.__animating = self.tweener.hasTweens()
+ if not self.window: #will wait until window comes
+ return self.__animating
+
+
+ time_since_start = (dt.datetime.now() - self.last_frame_time).microseconds / 1000000.0
+ self.tweener.update(time_since_start)
- def register_mouse_region(self, x1, y1, x2, y2, region_name):
- self.mouse_regions.append((x1, y1, x2, y2, region_name))
+ self.queue_draw()
+ self.window.process_updates(True)
- def redraw_canvas(self):
- """Force graph redraw"""
- if self.window: #this can get called before expose
- self.queue_draw()
- self.window.process_updates(True)
+ self.last_frame_time = dt.datetime.now()
+ return self.__animating
- def _render(self):
- raise NotImplementedError
- def set_value_range(self, x_min = None, x_max = None, y_min = None, y_max = None):
- """sets up our internal conversion matrix, because cairo one will
- scale also fonts and we need something in between!"""
-
- #store given params, we might redo the math later
- if not self.value_boundaries:
- self.value_boundaries = [x_min, x_max, y_min, y_max]
- else:
- if x_min != None:
- self.value_boundaries[0] = x_min
- if x_max != None:
- self.value_boundaries[1] = x_max
- if y_min != None:
- self.value_boundaries[2] = y_min
- if y_max != None:
- self.value_boundaries[3] = y_max
- self.x_factor, self.y_factor = None, None
- self._get_factors()
-
- def _get_factors(self):
- if not self.x_factor:
- self.x_factor = 1
- if self.value_boundaries and self.value_boundaries[0] != None and self.value_boundaries[1] != None:
- self.x_factor = float(self.graph_width or self.width) / abs(self.value_boundaries[1] - self.value_boundaries[0])
-
- if not self.y_factor:
- self.y_factor = 1
- if self.value_boundaries and self.value_boundaries[2] != None and self.value_boundaries[3] != None:
- self.y_factor = float(self.graph_height or self.height) / abs(self.value_boundaries[3] - self.value_boundaries[2])
-
- return self.x_factor, self.y_factor
-
-
- def get_pixel(self, x_value = None, y_value = None):
- """returns screen pixel position for value x and y. Useful to
- get and then pad something
-
- x = min1 + (max1 - min1) * (x / abs(max2-min2))
- => min1 + const1 * x / const2
- => const3 = const1 / const2
- => min + x * const3
- """
- x_factor, y_factor = self._get_factors()
-
- if x_value != None:
- if self.value_boundaries and self.value_boundaries[0] != None:
- if self.value_boundaries[1] > self.value_boundaries[0]:
- x_value = self.value_boundaries[0] + x_value * x_factor
- else: #case when min is larger than max (flipped)
- x_value = self.value_boundaries[1] - x_value * x_factor
- if y_value is None:
- return x_value + self.graph_x
-
- if y_value != None:
- if self.value_boundaries and self.value_boundaries[2] != None:
- if self.value_boundaries[3] > self.value_boundaries[2]:
- y_value = self.value_boundaries[2] + y_value * y_factor
- else: #case when min is larger than max (flipped)
- y_value = self.value_boundaries[2] - y_value * y_factor
- if x_value is None:
- return y_value + self.graph_y
-
- return x_value + self.graph_x, y_value + self.graph_y
+ def animate(self, object, params = {}, duration = None, easing = None, callback = None):
+ if duration: params["tweenTime"] = duration # if none will fallback to tweener's default
+ if easing: params["tweenType"] = easing # if none will fallback to tweener's default
+ if callback: params["onCompleteFunction"] = callback
+ self.tweener.addTween(object, **params)
+ self.redraw_canvas()
+
- def get_value_at_pos(self, x = None, y = None):
- """returns mapped value at the coordinates x,y"""
- x_factor, y_factor = self._get_factors()
-
- if x != None:
- x = (x - self.graph_x) / x_factor
- if y is None:
- return x
- if y != None:
- y = (y - self.graph_x) / y_factor
- if x is None:
- return y
- return x, y
-
+ """ drawing on canvas bits """
def __rectangle(self, x, y, w, h, color, opacity = 0):
if color[0] > 1: color = [c / 256.0 for c in color]
@@ -202,32 +119,57 @@ class Area(gtk.DrawingArea):
def fill_rectangle(self, x, y, w, h, color, opacity = 0):
self.context.save()
self.__rectangle(x, y, w, h, color, opacity)
- self.context.fill()
- self.__rectangle(x, y, w, h, color, 0)
+ self.context.fill_preserve()
+ self.set_color(color)
self.context.stroke()
self.context.restore()
- def longest_label(self, labels):
- """returns width of the longest label"""
- max_extent = 0
- for label in labels:
- self.layout.set_text(label)
- label_w, label_h = self.layout.get_pixel_size()
- max_extent = max(label_w + 5, max_extent)
+ def set_text(self, text):
+ # sets text and returns width and height of the layout
+ self.layout.set_text(text)
+ return self.layout.get_pixel_size()
- return max_extent
-
- def move_to(self, x, y):
- """our copy of moveto that takes into account our transformations"""
- self.context.move_to(*self.get_pixel(x, y))
+ def set_color(self, color, opacity = None):
+ color = Colors.normalize_rgb(color)
+
+ if opacity:
+ self.context.set_source_rgba(color[0], color[1], color[2], opacity)
+ elif len(color) == 3:
+ self.context.set_source_rgb(*color)
+ else:
+ self.context.set_source_rgba(*color)
- def line_to(self, x, y):
- self.context.line_to(*self.get_pixel(x, y))
+
+ def register_mouse_region(self, x1, y1, x2, y2, region_name):
+ self.mouse_regions.append((x1, y1, x2, y2, region_name))
+
+ """ exposure events """
+ def do_configure_event(self, event):
+ (self.__width, self.__height) = self.window.get_size()
+ self.queue_draw()
+
+ def do_expose_event(self, event):
+ self.width, self.height = self.window.get_size()
+ self.context = self.window.cairo_create()
+
+
+ self.context.set_antialias(cairo.ANTIALIAS_NONE)
+ self.context.rectangle(event.area.x, event.area.y,
+ event.area.width, event.area.height)
+ self.context.clip()
+
+ self.layout = self.context.create_layout()
+ default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+ default_font.set_size(self.font_size * pango.SCALE)
+ self.layout.set_font_description(default_font)
+ alloc = self.get_allocation() #x, y, width, height
+ self.width, self.height = alloc.width, alloc.height
- def __on_mouse_out(self, area, event):
- self.__prev_mouse_regions = None
- self.emit("mouse-over", [])
+ self.mouse_regions = [] #reset since these can move in each redraw
+ self.on_expose()
+
+ """ mouse events """
def __on_mouse_move(self, area, event):
if not self.mouse_regions:
return
@@ -254,6 +196,10 @@ class Area(gtk.DrawingArea):
self.__prev_mouse_regions = mouse_regions
+ def __on_mouse_out(self, area, event):
+ self.__prev_mouse_regions = None
+ self.emit("mouse-over", [])
+
def __on_button_release(self, area, event):
if not self.mouse_regions:
return
@@ -270,62 +216,72 @@ class Area(gtk.DrawingArea):
if mouse_regions:
self.emit("button-release", mouse_regions)
+
-class Integrator(object):
- """an iterator, inspired by "visualizing data" book to simplify animation"""
- def __init__(self, start_value, damping = 0.5, attraction = 0.2):
- #if we got datetime, convert it to unix time, so we operate with numbers again
- self.current_value = start_value
- if isinstance(start_value, dt.datetime):
- self.current_value = int(time.mktime(start_value.timetuple()))
-
- self.value_type = type(start_value)
-
- self.target_value = start_value
- self.current_frame = 0
-
- self.targeting = False
- self.vel, self.accel, self.force = 0, 0, 0
- self.mass = 1
- self.damping = damping
- self.attraction = attraction
-
- def __repr__(self):
- current, target = self.current_value, self.target_value
- if self.value_type == dt.datetime:
- current = dt.datetime.fromtimestamp(current)
- target = dt.datetime.fromtimestamp(target)
- return "<Integrator %s, %s>" % (current, target)
+
+""" simple example """
+class SimpleAnimation(Area):
+ def __init__(self):
+ Area.__init__(self)
+ self.rect_x, self.rect_y = 10.5, 10.5
+ self.rect_width, self.rect_height = 50, 50
- def target(self, value):
- """target next value"""
- self.targeting = True
- self.target_value = value
- if isinstance(value, dt.datetime):
- self.target_value = int(time.mktime(value.timetuple()))
+ def on_expose(self):
+ # on expose is called when we are ready to draw
- def update(self):
- """goes from current to target value
- if there is any action needed. returns velocity, which is synonym from
- delta. Use it to determine when animation is done (experiment to find
- value that fits you!"""
-
- if self.targeting:
- self.force += self.attraction * (self.target_value - self.current_value)
-
- self.accel = self.force / self.mass
- self.vel = (self.vel + self.accel) * self.damping
- self.current_value += self.vel
- self.force = 0
- return abs(self.vel)
-
- def finish(self):
- self.current_value = self.target_value
+ # fill_area is just a shortcut function
+ # feel free to use self.context. move_to, line_to and others
+ self.fill_area(self.rect_x,
+ self.rect_y,
+ self.rect_width,
+ self.rect_height, (168, 186, 136))
+
+class BasicWindow:
+ # close the window and quit
+ def delete_event(self, widget, event, data=None):
+ gtk.main_quit()
+ return False
+
+ def __init__(self):
+ # Create a new window
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
- @property
- def value(self):
- if self.value_type == dt.datetime:
- return dt.datetime.fromtimestamp(self.current_value)
- else:
- return self.current_value
-
+ self.window.set_title("Graphics Module")
+ self.window.set_size_request(500, 500)
+ self.window.connect("delete_event", self.delete_event)
+
+ self.graphic = SimpleAnimation()
+
+ box = gtk.VBox()
+ box.pack_start(self.graphic)
+
+ button = gtk.Button("Hello")
+ button.connect("clicked", self.on_go_clicked)
+
+ box.add_with_properties(button, "expand", False)
+
+ self.window.add(box)
+ self.window.show_all()
+
+
+ def on_go_clicked(self, widget):
+ import random
+
+ # set x and y to random position within the drawing area
+ x = round(min(random.random() * self.graphic.width,
+ self.graphic.width - self.graphic.rect_width))
+ y = round(min(random.random() * self.graphic.height,
+ self.graphic.height - self.graphic.rect_height))
+
+ # here we call the animate function with parameters we would like to change
+ # the easing functions outside graphics module can be accessed via
+ # graphics.Easing
+ self.graphic.animate(self.graphic,
+ dict(rect_x = x, rect_y = y),
+ duration = 0.8,
+ easing = Easing.Elastic.easeOut)
+
+if __name__ == "__main__":
+ example = BasicWindow()
+ gtk.main()
+
\ No newline at end of file
diff --git a/src/gui/widgets/pytweener.py b/src/gui/widgets/pytweener.py
index 790b88f..113b4a5 100644
--- a/src/gui/widgets/pytweener.py
+++ b/src/gui/widgets/pytweener.py
@@ -90,6 +90,11 @@ class Tweener:
if t.target is obj:
t.complete = True
+ def finish(self):
+ #go to last frame for all tweens
+ for t in self.currentTweens:
+ t.update(t.duration)
+ self.currentTweens = []
def update(self, timeSinceLastFrame):
removable = []
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]