from __future__ import division from ..tracker import GestureTracker, Gesture from ..geometry import Positionable, MovingPositionable class RotationGesture(Gesture, Positionable): """ A rotation gesture has a angle in radians and a rotational centroid. """ _type = 'rotate' def __init__(self, event, centroid, angle, n=1): Gesture.__init__(self, event) Positionable.__init__(self, *centroid.get_position()) self.angle = angle self.n = n def __str__(self): return '' \ % (self.x, self.y, self.angle) def get_angle(self): return -self.angle class PinchGesture(Gesture, Positionable): """ A pinch gesture has a scale (1.0 means no scaling) and a centroid from which the scaling originates. """ _type = 'pinch' def __init__(self, event, centroid, scale, n=1): Gesture.__init__(self, event) Positionable.__init__(self, *centroid.get_position()) self.scale = scale self.n = n def __str__(self): return '' \ % (self.x, self.y, self.scale) def get_scale(self): return self.scale class DragGesture(Gesture, Positionable): """ A momevent gesture has an initial position, and a translation from that position. """ _type = 'drag' def __init__(self, event, initial_position, translation, n=1): Gesture.__init__(self, event) Positionable.__init__(self, *initial_position.get_position()) self.translation = translation self.n = n def __str__(self): x, y = self tx, ty = self.translation return '' \ % (x, y, tx, ty, self.n) def get_translation(self): return self.translation class FlickGesture(Gesture, Positionable): _type = 'flick' def __init__(self, event, initial_position, translation): Gesture.__init__(self, event) Positionable.__init__(self, *initial_position.get_position()) self.translation = translation def __str__(self): x, y = self.get_position() tx, ty = self.translation return '' \ % (x, y, tx, ty) def get_translation(self): return self.translation class TransformationTracker(GestureTracker): """ Tracker for linear transformations. This implementation detects rotation, scaling and translation using the centroid of all touch points. """ supported_gestures = [RotationGesture, PinchGesture, DragGesture, FlickGesture] def __init__(self, area): super(TransformationTracker, self).__init__(area) # All touch points performing the transformation self.points = [] # Current and previous centroid of all touch points self.centroid = None self.deleted = [] def update_centroid(self): if not self.points: self.centroid = None return # Calculate average touch point coordinates l = len(self.points) coords = [p.get_position() for p in self.points] all_x, all_y = zip(*coords) x = sum(all_x) / l y = sum(all_y) / l # Update centroid positionable if self.centroid: self.centroid.set_position(x, y) else: self.centroid = MovingPositionable(x, y) def on_point_down(self, event): self.points.append(event.point) self.update_centroid() event.stop_propagation() def on_point_move(self, event): point = event.point if point not in self.points: pid = point.get_id() if pid not in self.deleted: return self.debug('recovered %s' % point) self.deleted.remove(pid) self.points.append(point) self.update_centroid() event.stop_propagation() self.invalidate_points() l = len(self.points) if l > 1: offset_centroid = self.centroid - self.area.get_screen_offset() # Rotation (around the previous centroid) rotation = point.rotation_around(self.centroid) / l self.trigger(RotationGesture(event, offset_centroid, rotation, l)) # Scale prev = self.centroid.distance_to(point.get_previous_position()) dist = self.centroid.distance_to(point) dist = prev + (dist - prev) / l scale = dist / prev self.trigger(PinchGesture(event, offset_centroid, scale, l)) # Update centroid before movement can be detected self.update_centroid() # Movement using multiple touch points self.trigger(DragGesture(event, self.centroid, self.centroid.translation(), l)) def on_point_up(self, event): if event.point in self.points: self.points.remove(event.point) if not self.points: self.trigger(FlickGesture(event, self.centroid, self.centroid.translation())) self.update_centroid() event.stop_propagation() def invalidate_points(self): """ Check if all points are still in the corresponding area, and remove those which are not. """ delete = [] if self.area.parent: ox, oy = self.area.parent.get_screen_offset() else: ox = oy = 0 for i, p in enumerate(self.points): x, y = p.get_position() if not self.area.contains(x - ox, y - oy): self.debug('deleted %s' % p) delete.append(i) self.deleted.append(p.get_id()) if delete: self.points = [p for i, p in enumerate(self.points) if i not in delete] self.update_centroid()