Package libavg :: Module grabbable

Source Code for Module libavg.grabbable

  1  # libavg - Media Playback Engine. 
  2  # Copyright (C) 2003-2008 Ulrich von Zadow 
  3  # 
  4  # This library is free software; you can redistribute it and/or 
  5  # modify it under the terms of the GNU Lesser General Public 
  6  # License as published by the Free Software Foundation; either 
  7  # version 2 of the License, or (at your option) any later version. 
  8  # 
  9  # This library is distributed in the hope that it will be useful, 
 10  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 12  # Lesser General Public License for more details. 
 13  # 
 14  # You should have received a copy of the GNU Lesser General Public 
 15  # License along with this library; if not, write to the Free Software 
 16  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 17  # 
 18  # Current versions can be found at www.libavg.de 
 19  # 
 20  # Original author of this file is Martin Heistermann <mh at sponc dot de> 
 21  # 
 22   
 23  import math 
 24   
 25  from libavg import avg, Point2D 
 26  from clusteredEventList import ClusteredEventList 
 27  from mathutil import solveEquationMatrix, EquationSingular, EquationNotSolvable 
 28  from mathutil import getAngle, getDistance, getScaledDim, getOffsetForMovedPivot 
 29   
 30   
 31  NUM_SPEEDS = 5 
 32   
 33  g_player = avg.Player.get() 
 34   
35 -def getRelPos(pos, nodePos, angle, pivot):
36 return (pos - nodePos).getRotated(-angle, pivot)
37
38 -def getAbsPos(pos, nodePos, angle, pivot):
39 return Point2D(pos).getRotated(angle, Point2D(pivot)) + Point2D(nodePos)
40 41
42 -class Grabbable:
43 """Helper for multitouch object movement. 44 Grabbable will add the well-known multitouch gestures 45 to a node, i.e. moving with one finger, rotating, 46 resizing and moving with 2 fingers. 47 It also works with many more cursors/fingers, clustering 48 them. 49 """
50 - def __init__(self, 51 node, 52 source = avg.TOUCH | avg.MOUSE, 53 maxSize = None, minSize = None, 54 onResize = lambda: None, 55 onDrop = lambda: None, 56 onStop = lambda: None, 57 onAction = lambda: None, 58 onMotion = lambda pos, size, angle, pivot: None, 59 onResetMotion = lambda: None, 60 onDragStart = lambda: None, 61 initialDown = None, 62 inertia = 0.95, 63 torque = 0.95, 64 moveNode = True):
65 66 global g_player 67 self.__inertia = inertia 68 self.__torque = torque 69 self.__node = node 70 self.__minSize = minSize 71 self.__maxSize = maxSize 72 self.__moveNode = moveNode 73 self.__callback = { 74 'onResize': onResize, 75 'onDrop': onDrop, 76 'onStop': onStop, 77 'onAction': onAction, 78 'onMotion': onMotion, 79 'onResetMotion': onResetMotion, 80 'onDragStart': onDragStart, 81 } 82 83 self.__stopInertia() 84 #self.__oldAngle = 0.0 85 self.__lastFixPoint = None 86 self.__lastNumFingers = 0 87 self.__onFrameHandler = g_player.setOnFrameHandler(self.__onFrame) 88 89 self.__eventList = ClusteredEventList (self.__node, 90 source = source, 91 onMotion = self.__onMotion, 92 onUp = self.__onUp, 93 onDown = self.__onDown, 94 resetMotion = self.__resetMotion 95 ) 96 if initialDown: 97 self.__eventList.handleInitialDown (initialDown) 98 self.__lastPos = None 99 self.__stopped = True
100
101 - def isDragging(self):
102 return len(self.__eventList) > 0
103
104 - def getSpeed(self):
105 return self.__speed
106
107 - def getAngle(self):
108 return self.__angle
109
110 - def delete(self):
111 global g_player 112 g_player.clearInterval(self.__onFrameHandler) 113 self.__eventList.delete() 114 self.__node = None
115
116 - def __onFrame(self):
117 global g_player 118 if len (self.__eventList) == 0: 119 # inertia 120 if self.__speed.getNorm() < 1: 121 if not self.__stopped: 122 pos = Point2D(round(self.__node.pos.x), round(self.__node.pos.y)) 123 size = Point2D(round(self.__node.size.x), round(self.__node.size.y)) 124 self.__callback['onMotion'](pos, size, self.__node.angle, 125 self.__node.pivot) 126 self.__stopped = True 127 self.__callback['onStop']() 128 self.__stopInertia() 129 return 130 131 pos = self.__node.pos + self.__speed 132 angle = self.__node.angle + self.__rotSpeed 133 size = self.__node.size 134 pivot = self.__node.pivot 135 136 if self.__moveNode: 137 self.__node.pos = pos 138 self.__node.size = size 139 self.__node.angle = angle 140 self.__node.pivot = pivot 141 142 self.__callback['onMotion'](pos, size, angle, pivot) 143 144 self.__speed *= self.__inertia 145 self.__rotSpeed *= self.__torque 146 else: 147 self.__stopped = False 148 # save current speed for inertia 149 pos = self.__node.pos 150 angle = self.__node.angle 151 if self.__lastPos: 152 speed = pos - self.__lastPos 153 rotSpeed = angle - self.__lastAngle 154 # use minimal angle to avoid extreme rotation: 155 if rotSpeed < -math.pi/2: 156 rotSpeed += math.pi*2 157 if rotSpeed > math.pi/2: 158 rotSpeed -= math.pi*2 159 self.__speeds = (self.__speeds + [speed])[-NUM_SPEEDS:] 160 self.__rotSpeeds = (self.__rotSpeeds + [rotSpeed])[-NUM_SPEEDS:] 161 self.__lastPos = pos 162 self.__lastAngle = angle
163
164 - def __stopInertia (self):
165 self.__speed = Point2D(0,0) 166 self.__rotSpeed = 0 167 self.__speeds = [Point2D(0,0)] 168 self.__rotSpeeds = [0.0] 169 self.__lastPos = None
170
171 - def __onDown(self):
172 def __gotoTopLayer(): 173 parent = self.__node.getParent() 174 numChildren = parent.getNumChildren() 175 parent.reorderChild(self.__node, numChildren - 1)
176 177 self.__stopInertia() 178 self.__reshape() 179 if len(self.__eventList) == 1: # first finger down 180 if self.__moveNode: 181 __gotoTopLayer() 182 self.__callback['onDragStart']() 183 self.__callback['onAction']()
184
185 - def __onMotion(self):
186 self.__reshape() 187 self.__callback['onAction']()
188
189 - def __drop(self):
190 self.__speed = sum(self.__speeds, Point2D(0,0)) / len(self.__speeds) 191 self.__rotSpeed = sum(self.__rotSpeeds, 0.0) / len(self.__rotSpeeds) 192 self.__callback['onDrop']()
193
194 - def __onUp(self):
195 if len(self.__eventList) == 0: 196 self.__drop() 197 self.__callback['onAction']()
198
199 - def __reshape (self):
200 def applyAffineTransformation (parameters, p): 201 """apply transformation to point: 202 M: v: 203 P' = P * (m -n) + (v0) 204 (n m) (v1) """ 205 m, n, v0, v1 = parameters 206 nx = p[0] * m - p[1] * n + v0 207 ny = p[0] * n + p[1] * m + v1 208 return Point2D(nx, ny)
209 210 clusters = self.__eventList.getCursors() 211 212 if len(clusters) == 1: 213 cursor = self.__eventList.getCursors()[0] 214 215 pos = self.__oldpos + cursor.getDelta() 216 size = self.__node.size 217 angle = self.__node.angle 218 pivot = self.__node.pivot 219 220 elif len(clusters) == 2: 221 # calculate an affine transformation which describes cluster movement 222 # TODO: explain used maths 223 cursor1, cursor2 = clusters 224 equationMatrix = ( 225 ((cursor1.getStartPos().x, -cursor1.getStartPos().y, 1, 0), cursor1.getPos().x), 226 ((cursor1.getStartPos().y, cursor1.getStartPos().x, 0, 1), cursor1.getPos().y), 227 ((cursor2.getStartPos().x, -cursor2.getStartPos().y, 1, 0), cursor2.getPos().x), 228 ((cursor2.getStartPos().y, cursor2.getStartPos().x, 0, 1), cursor2.getPos().y), 229 ) 230 try: 231 param = solveEquationMatrix (equationMatrix) 232 except (EquationSingular, EquationNotSolvable): 233 print "WARNING: Grabbable: cannot solve equation, skipping movement" 234 return False 235 236 absTouchCenter = cursor2.getPos() + (cursor1.getPos() - cursor2.getPos()) / 2 237 238 n_tl = applyAffineTransformation (param, self.o_tl) 239 n_bl = applyAffineTransformation (param, self.o_bl) 240 n_tr = applyAffineTransformation (param, self.o_tr) 241 242 pos = n_tl 243 if pos.x > 1e6 and pos.x > self.o_tl.x: 244 pos = self.o_tl 245 if pos.y > 1e6 and pos.y > self.o_tl.y: 246 pos = self.o_tl 247 if pos.x < -1e6 and pos.x < self.o_tl.x: 248 pos = self.o_tl 249 if pos.y < -1e6 and pos.y < self.o_tl.y: 250 pos = self.o_tl 251 252 size = Point2D(getDistance (n_tl, n_tr), getDistance (n_tl, n_bl)) 253 if size.x<=0.1: 254 size.x=0.1 255 if size.y<=0.1: 256 size.y=0.1 257 angle = getAngle(n_tl, n_tr) # pivot for this rotation is 0,0 (rel. to node) 258 259 # get middle of touches relative to the node 260 relTouchCenter = getRelPos(absTouchCenter, pos, angle, Point2D(0,0)) 261 pos += getOffsetForMovedPivot( 262 oldPivot = Point2D(0,0), 263 newPivot = relTouchCenter, 264 angle = angle) 265 pivot = relTouchCenter 266 267 def scaleMinMax(pos, size, angle, pivot): 268 # the absolute position of the pivot should change when 269 # resizing, so we track and revert absolute pivot movement 270 oldAbsPivot = getAbsPos(pivot, pos, angle, pivot) 271 272 newSize = getScaledDim (size, 273 max = self.__maxSize, 274 min = self.__minSize) 275 276 ratio = newSize.x / size.x 277 newPivot = pivot * ratio 278 279 pos += getOffsetForMovedPivot( 280 oldPivot = pivot, 281 newPivot = newPivot, 282 angle = angle) 283 pivot = newPivot 284 285 size = newSize 286 287 newAbsPivot = getAbsPos(pivot, pos, angle, pivot) 288 289 # move node to revert absolute pivot position 290 pos -= (newAbsPivot - oldAbsPivot) 291 292 return (pos, size, angle, pivot) 293 294 (pos, size, angle, pivot) = scaleMinMax(pos, size, angle, pivot) 295 296 else: # more than 2 clusters 297 assert False 298 299 if self.__moveNode: 300 self.__node.angle = angle 301 self.__node.size = size 302 self.__node.pos = pos 303 self.__node.pivot = pivot 304 self.__callback['onResize']() 305 306 self.__callback['onMotion'](pos, size, angle, pivot) 307
308 - def __resetMotion (self):
309 """ sets a new movement start point """ 310 # store absolute position at the beginning of the movement 311 # for 2-finger-movements: 312 def getPos(pos): 313 return getAbsPos(pos, self.__node.pos, self.__node.angle, self.__node.pivot)
314 self.o_tl = getPos((0, 0)) 315 self.o_bl = getPos((0, self.__node.height)) 316 self.o_tr = getPos((self.__node.width, 0)) 317 318 # for 1-finger-movements: 319 self.__oldpos = self.__node.pos 320 321 self.__lastFixPoint = None 322 self.__callback['onResetMotion']() 323