transform.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. from __future__ import division
  2. from ..tracker import GestureTracker, Gesture
  3. from ..geometry import Positionable, MovingPositionable
  4. class RotationGesture(Gesture, Positionable):
  5. """
  6. A rotation gesture has a angle in radians and a rotational centroid.
  7. """
  8. _type = 'rotate'
  9. def __init__(self, event, centroid, angle, n=1):
  10. Gesture.__init__(self, event)
  11. Positionable.__init__(self, *centroid.get_position())
  12. self.angle = angle
  13. self.n = n
  14. def __str__(self):
  15. return '<RotationGesture at (%s, %s) angle=%s>' \
  16. % (self.x, self.y, self.angle)
  17. def get_angle(self):
  18. return -self.angle
  19. class PinchGesture(Gesture, Positionable):
  20. """
  21. A pinch gesture has a scale (1.0 means no scaling) and a centroid from
  22. which the scaling originates.
  23. """
  24. _type = 'pinch'
  25. def __init__(self, event, centroid, scale, n=1):
  26. Gesture.__init__(self, event)
  27. Positionable.__init__(self, *centroid.get_position())
  28. self.scale = scale
  29. self.n = n
  30. def __str__(self):
  31. return '<PinchGesture at (%s, %s) scale=%s>' \
  32. % (self.x, self.y, self.scale)
  33. def get_scale(self):
  34. return self.scale
  35. class DragGesture(Gesture, Positionable):
  36. """
  37. A momevent gesture has an initial position, and a translation from that
  38. position.
  39. """
  40. _type = 'drag'
  41. def __init__(self, event, initial_position, translation, n=1):
  42. Gesture.__init__(self, event)
  43. Positionable.__init__(self, *initial_position.get_position())
  44. self.translation = translation
  45. self.n = n
  46. def __str__(self):
  47. x, y = self
  48. tx, ty = self.translation
  49. return '<DragGesture at (%s, %s) translation=(%s, %s) n=%d>' \
  50. % (x, y, tx, ty, self.n)
  51. def get_translation(self):
  52. return self.translation
  53. class FlickGesture(Gesture, Positionable):
  54. _type = 'flick'
  55. def __init__(self, event, initial_position, translation):
  56. Gesture.__init__(self, event)
  57. Positionable.__init__(self, *initial_position.get_position())
  58. self.translation = translation
  59. def __str__(self):
  60. x, y = self.get_position()
  61. tx, ty = self.translation
  62. return '<FlickGesture at (%s, %s) translation=(%s, %s)>' \
  63. % (x, y, tx, ty)
  64. def get_translation(self):
  65. return self.translation
  66. class TransformationTracker(GestureTracker):
  67. """
  68. Tracker for linear transformations. This implementation detects rotation,
  69. scaling and translation using the centroid of all touch points.
  70. """
  71. supported_gestures = [RotationGesture, PinchGesture, DragGesture,
  72. FlickGesture]
  73. def __init__(self, area):
  74. super(TransformationTracker, self).__init__(area)
  75. # All touch points performing the transformation
  76. self.points = []
  77. # Current and previous centroid of all touch points
  78. self.centroid = None
  79. self.deleted = []
  80. def update_centroid(self):
  81. if not self.points:
  82. self.centroid = None
  83. return
  84. # Calculate average touch point coordinates
  85. l = len(self.points)
  86. coords = [p.get_position() for p in self.points]
  87. all_x, all_y = zip(*coords)
  88. x = sum(all_x) / l
  89. y = sum(all_y) / l
  90. # Update centroid positionable
  91. if self.centroid:
  92. self.centroid.set_position(x, y)
  93. else:
  94. self.centroid = MovingPositionable(x, y)
  95. def on_point_down(self, event):
  96. self.points.append(event.point)
  97. self.update_centroid()
  98. event.stop_propagation()
  99. def on_point_move(self, event):
  100. point = event.point
  101. if point not in self.points:
  102. pid = point.get_id()
  103. if pid not in self.deleted:
  104. return
  105. self.debug('recovered %s' % point)
  106. self.deleted.remove(pid)
  107. self.points.append(point)
  108. self.update_centroid()
  109. event.stop_propagation()
  110. self.invalidate_points()
  111. l = len(self.points)
  112. if l > 1:
  113. offset_centroid = self.centroid - self.area.get_screen_offset()
  114. # Rotation (around the previous centroid)
  115. rotation = point.rotation_around(self.centroid) / l
  116. self.trigger(RotationGesture(event, offset_centroid, rotation, l))
  117. # Scale
  118. prev = self.centroid.distance_to(point.get_previous_position())
  119. dist = self.centroid.distance_to(point)
  120. dist = prev + (dist - prev) / l
  121. scale = dist / prev
  122. self.trigger(PinchGesture(event, offset_centroid, scale, l))
  123. # Update centroid before movement can be detected
  124. self.update_centroid()
  125. # Movement using multiple touch points
  126. self.trigger(DragGesture(event, self.centroid,
  127. self.centroid.translation(), l))
  128. def on_point_up(self, event):
  129. if event.point in self.points:
  130. self.points.remove(event.point)
  131. if not self.points:
  132. self.trigger(FlickGesture(event, self.centroid,
  133. self.centroid.translation()))
  134. self.update_centroid()
  135. event.stop_propagation()
  136. def invalidate_points(self):
  137. """
  138. Check if all points are still in the corresponding area, and remove
  139. those which are not.
  140. """
  141. delete = []
  142. if self.area.parent:
  143. ox, oy = self.area.parent.get_screen_offset()
  144. else:
  145. ox = oy = 0
  146. for i, p in enumerate(self.points):
  147. x, y = p.get_position()
  148. if not self.area.contains(x - ox, y - oy):
  149. self.debug('deleted %s' % p)
  150. delete.append(i)
  151. self.deleted.append(p.get_id())
  152. if delete:
  153. self.points = [p for i, p in enumerate(self.points)
  154. if i not in delete]
  155. self.update_centroid()