Mercurial > MadButterfly
view pyink/tween.py @ 1457:416a18409603
Show an empty document in SVG viewer widget
author | Thinker K.F. Li <thinker@codemud.net> |
---|---|
date | Sat, 16 Apr 2011 22:25:35 +0800 |
parents | f89d3ee130de |
children |
line wrap: on
line source
# -*- indent-tabs-mode: t; tab-width: 8; python-indent: 4; -*- # vim: sw=4:ts=8:sts=4 import traceback import math def _shift_matrix(x, y): return (1, 0, 0, 1, x, y) def _rotate_matrix(a): return (math.cos(a), math.sin(a), -math.sin(a), math.cos(a), 0, 0) def _scale_matrix(scale_x, scale_y): return (scale_x, 0, 0, scale_y, 0, 0) _id_matrix = (1, 0, 0, 1, 0, 0) def _mulA(a, b): return (a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1], a[0] * b[2] + a[2] * b[3], a[1] * b[2] + a[3] * b[3], a[0] * b[4] + a[2] * b[5] + a[4], a[1] * b[4] + a[3] * b[5] + a[5]) def _parse_style(style): attrs = {} style_parts = style.split(';') for part in style_parts: part = part.strip() if not part: continue nv_pair = part.split(':') if len(nv_pair) != 2: raise ValueError, 'invalid format for style "%s"' % (style) name = nv_pair[0].strip() value = nv_pair[1].strip() attrs[name] = value pass return attrs def _gen_style(attrs): parts = [name + ':' + value for name, value in attrs.items()] style = ';'.join(parts) return style def _parse_transform_str(txt): if txt[0:9] == 'translate': fields = txt[10:].split(',') x = float(fields[0]) fields = fields[1].split(')') y = float(fields[0]) return [1, 0, 0, 1 , x, y] elif txt[0:6] == 'matrix': fields = txt[7:].split(')') fields = fields[0].split(',') return [float(field) for field in fields] pass ## \brief Parse style attributes about animation. # def _parse_style_ani(node, ani_attrs): try: style = node.getAttribute('style') except: # has no style style_attrs = {} else: style_attrs = _parse_style(style) pass if 'opacity' in style_attrs: ani_attrs['opacity'] = float(style_attrs['opacity']) pass if 'display' in style_attrs: ani_attrs['display'] = style_attrs['display'] != 'none' pass pass ## \brief Parse all attributes about animation # def _parse_attr_ani(node, ani_attrs): def _parse_transform_with_center(attr_value): value = _parse_transform_str(attr_value) x, y = node.spitem.getCenter() return (value, (x, y)) attr_defs = {'x': float, 'y': float, 'width': float, 'height': float, 'transform': _parse_transform_with_center} for attrname, parser in attr_defs.items(): try: value = node.getAttribute(attrname) except: # has no this attribute pass else: parsed_value = parser(value) ani_attrs[attrname] = parsed_value pass pass pass ## \brief Interpolate float values. # def _interp_float(start_value, stop_value, percent): if start_value == None or stop_value == None: if percent == 1: return stop_value return start_value return start_value * (1 - percent) + stop_value * percent ## \brief Interpolate matric. # def _interp_transform(start_value, stop_value, percent): start_matrix = start_value[0] start_center_x, start_center_y = start_value[1] stop_matrix = stop_value[0] stop_center_x, stop_center_y = stop_value[1] start_scale_x, start_scale_y, start_ang, start_x, start_y = \ _decomposition(start_matrix) stop_scale_x, stop_scale_y, stop_ang, stop_x, stop_y = \ _decomposition(stop_matrix) interp = lambda x, y: _interp_float(x, y, percent) factor_x = interp(start_scale_x, stop_scale_x) / start_scale_x factor_y = interp(start_scale_y, stop_scale_y) / start_scale_y angle = interp(start_ang, stop_ang) shift_x = interp(start_center_x, stop_center_x) shift_y = interp(start_center_y, stop_center_y) # Shift center point back to origin matrix = start_matrix shift_matrix = _shift_matrix(-start_center_x, -start_center_y) matrix = _mulA(shift_matrix, matrix) # Remove rotation rotate_matrix = _rotate_matrix(-start_ang) matrix = _mulA(rotate_matrix, matrix) # Apply new scaling scale_matrix = _scale_matrix(factor_x, factor_y) matrix = _mulA(scale_matrix, matrix) # Rotate to new angle rotate_matrix = _rotate_matrix(angle) matrix = _mulA(rotate_matrix, matrix) # Shift space to aim center point on new position. shift_matrix = _shift_matrix(shift_x, shift_y) matrix = _mulA(shift_matrix, matrix) return matrix ## \brief Interpolate for value of display style. # def _interp_display(start_value, stop_value, percent): if percent < 1: return start_value return stop_value _interp_funcs = { 'x': _interp_float, 'y': _interp_float, 'width': _interp_float, 'height': _interp_float, 'opacity': _interp_float, 'display': _interp_display, 'transform': _interp_transform} def _tween_interpolation(attrname, start_value, stop_value, percent): interp = _interp_funcs[attrname] _interp_value = interp(start_value, stop_value, percent) return _interp_value def _apply_animation_attrs(ani_attrs, node): for attr in ('x', 'y', 'width', 'height'): if attr in ani_attrs: node.setAttribute(attr, str(ani_attrs[attr])) pass pass if 'transform' in ani_attrs: try: style = node.getAttribute('style') except: style = '' pass transform = [str(elm) for elm in ani_attrs['transform']] transform_str = 'matrix(' + ','.join(transform) + ')' node.setAttribute('transform', transform_str) pass chg_style = [] for attrname in 'opacity display'.split(): if attrname in ani_attrs: chg_style.append((attrname, str(ani_attrs[attrname]))) pass pass if chg_style: try: style = node.getAttribute('style') except: style_attrs = chg_style else: style_attrs = _parse_style(style) style_attrs.update(dict(chg_style)) pass style = _gen_style(style_attrs) node.setAttribute('style', style) pass pass def _decomposition(m): """ Decompose the affine matrix into production of translation,rotation,shear and scale. The algorithm is documented at http://lists.w3.org/Archives/Public/www-style/2010Jun/0602.html """ if m[0]*m[3] == m[1]*m[2]: print "The affine matrix is singular" return [1,0,0,1,0,0] A=m[0] B=m[2] C=m[1] D=m[3] E=m[4] F=m[5] sx = math.sqrt(A*A+B*B) A = A/sx B = B/sx shear = m[0]*m[1]+m[2]*m[3] C = C - A*shear D = D - B*shear sy = math.sqrt(C*C+D*D) C = C/sy D = D/sy r = A*D-B*C if r == -1: shear = -shear sy = -sy pass R = math.atan2(-B,A) return [sx,sy, R, E, F] def _normalize_attrs(node1, attrs1, node2, attrs2): if node2.name() == 'svg:use': for name in 'x y width height'.split(): if name in attrs1: del attrs1[name] pass if name in attrs2: del attrs2[name] pass pass pass names = set(attrs1.keys() + attrs2.keys()) if 'transform' in names: if 'transform' not in attrs1: center = node1.spitem.getCenter() attrs1['transform'] = (_id_matrix, center) pass if 'transform' not in attrs2: center = node2.spitem.getCenter() attrs2['transform'] = (_id_matrix, center) pass root = node1.root() try: root_h = float(root.getAttribute('height')) except: root_h = 600 # 800x600 pass for attrs in (attrs1, attrs2): transform = attrs['transform'] center = (transform[1][0], root_h - transform[1][1]) attrs['transform'] = (transform[0], center) if 'x' in attrs: del attrs['x'] pass if 'y' in attrs: del attrs['y'] pass pass pass defaults = {'x': 0, 'y': 0, 'width': 0, 'height': 0, 'opacity': 1.0, 'display': 'inline'} for attrname in defaults: if attrname in names: if attrname not in attrs1: attrs1[attrname] = defaults[attrname] pass if attrname not in attrs2: attrs2[attrname] = defaults[attrname] pass pass pass if node2.name() == 'svg:use': accumulators = {'opacity': (lambda attr_v1, attr_v2: attr_v1 * attr_v2), 'transform': (lambda m1, m2: (_mulA(m2[0], m1[0]), m2[1])) } for attrname in accumulators: if attrname not in names: continue accumulator = accumulators[attrname] acc = accumulator(attrs1[attrname], attrs2[attrname]) attrs2[attrname] = acc pass pass pass class TweenObject(object): TWEEN_TYPE_NORMAL = 0 #TWEEN_TYPE_RELOCATE = 1 TWEEN_TYPE_SCALE = 1 def __init__(self, doc, root): super(TweenObject, self).__init__() self._doc = doc self._root = root try: self.width = float(root.getAttribute("width")) self.height = float(root.getAttribute("height")) except: self.width = 640 self.height = 480 pass pass def updateTweenContent(self, duplicate_group, tween_type, start_scene_group, stop_scene_group, percent): """ Update the content of the duplicate scene group. We will use precent, start_scene_group, stop_scene_group to compute transform matrix and update duplicate scene group specified. """ # Collect ref from the obj node = duplicate_group.firstChild() dup_nodes = {} while node: try: ref = node.getAttribute("ref") dup_nodes[ref] = node except: ref = None pass node = node.next() pass # Collect all nodes in stop scene stop_nodes = {} node = stop_scene_group.firstChild() while node: try: node_label = node.getAttribute("ns0:duplicate-src") stop_nodes[node_label] = node except: pass node = node.next() pass # Collect all nodes in start scene start_nodes = {} node = start_scene_group.firstChild() while node: try: node_label = node.getAttribute("id") start_nodes[node_label] = node except: pass node = node.next() pass # Remove duplicate nodes that is not in the set of start nodes for node_ref in dup_nodes: if node_ref not in start_nodes: node = dup_nodes[node_ref] duplicate_group.removeChild(node) pass pass # # Node ID of a node of start scene must be mapped to # 'ns0:duplicate-src' attribute of a node of stop scene. The # nodes which can not be mapped to a node of stop scene are # not manipulated by the tween. # # When a scene is duplicated, 'ns0:duplicate-src' attribute of # nodes, in the new scene, must be setted to ID of respective # one in the duplicated scene. # start_node = start_scene_group.firstChild() while start_node: start_node_id = start_node.getAttribute('id') dup_node = dup_nodes.setdefault(start_node_id, None) try: stop_node = stop_nodes[start_node_id] except KeyError: stop_node = start_node pass self.updateTweenObject(duplicate_group, tween_type, start_node, stop_node, percent, dup_node) start_node = start_node.next() pass pass def updateTweenObject(self, obj, typ, s, d, p, newobj): """ Generate tweened object in the @obj by using s and d in the @p percent http://lists.w3.org/Archives/Public/www-style/2010Jun/0602.html """ if typ == self.TWEEN_TYPE_SCALE: if newobj == None: newobj = s.duplicate(self._doc) newobj.setAttribute("ref", s.getAttribute("id")) obj.appendChild(newobj) pass self._update_tween_object_scale(s, d, p, newobj) pass elif typ == self.TWEEN_TYPE_NORMAL and newobj == None: newobj = s.duplicate(self._doc) newobj.setAttribute("ref", s.getAttribute("id")) obj.appendChild(newobj) pass pass def _update_tween_object_scale(self, start, stop, percent, newobj): start_attrs = {} _parse_style_ani(start, start_attrs) _parse_attr_ani(start, start_attrs) stop_attrs = {} _parse_style_ani(stop, stop_attrs) _parse_attr_ani(stop, stop_attrs) _normalize_attrs(start, start_attrs, stop, stop_attrs) tween_attrs = {} attrs = set(start_attrs.keys() + stop_attrs.keys()) for attr in attrs: start_v = start_attrs[attr] stop_v = stop_attrs[attr] if start_v != stop_v: new_v = _tween_interpolation(attr, start_v, stop_v, percent) tween_attrs[attr] = new_v pass pass _apply_animation_attrs(tween_attrs, newobj) pass pass ## \brief Providing capability of showing scenes. # # This class computes and shows scenes for a \ref domview_ui. The # content of layers and frames are read from domview_ui to generate # scenes properly. When caller requests to show a scene 'n', this # class compute content of frame 'n' for every layer of the # domview_ui. # class scenes_director(object): _tween_obj_tween_types = (TweenObject.TWEEN_TYPE_NORMAL, TweenObject.TWEEN_TYPE_SCALE) def __init__(self, domview_ui): super(scenes_director, self).__init__() self._domview = domview_ui self._tween_obj = TweenObject(domview_ui.doc, domview_ui.root) pass def show_scene(self, idx): """ Update the scene group according to the curretn scene data. There are a couple of cases. 1. If the type of the scene is normal, we display it when it contains the current frame. Otherwise hide it. 2. If the type of the scene is relocate or scale, we need to duplicate the scene group and then modify its transform matrix according to the definition of the scene. Then, hide the original scenr group and display the duplciate scene group. In addition, we may need to delete the old duplicated scene group as well. For each layer, we will always use the duplicated scene group whose name as dup. We will put the duplicated scene group inside it. We will create this group if it is not available. """ for layer_idx in range(self._domview.get_layer_num()): dup_group = self._domview.get_layer_dup_group(layer_idx) dup_group.setAttribute('style', 'display: none') all_key_tweens = self._domview.get_layer_keys(layer_idx) for start, end, tween_type in all_key_tweens: if start == idx: # at key frame scene_group = \ self._domview.get_key_group(layer_idx, start) scene_group.setAttribute('style', '') elif start < idx and end >= idx: # in Tween dup_group.setAttribute('style', '') scene_group = \ self._domview.get_key_group(layer_idx, start) scene_group.setAttribute('style', 'display: none') try: next_scene_group = \ self._domview.get_key_group(layer_idx, end + 1) except: # no next key frame next_scene_group = scene_group pass tween_obj_type = self._tween_obj_tween_types[tween_type] nframes = end - start + 1 percent = float(idx - start) / nframes self._tween_obj.updateTweenContent(dup_group, tween_obj_type, scene_group, next_scene_group, percent) pass else: # this scene should not be showed. scene_group = \ self._domview.get_key_group(layer_idx, start) scene_group.setAttribute('style', 'display: none') pass pass pass pass pass