
Share Your Code
Browse Code
- All (915)
-
(95)
-
(87)
-
(45)
-
(4)
-
(42)
-
(136)
-
(22)
-
(41)
-
(13)
-
(66)
-
(153)
-
(211)
Multi-Point Pinch Zoom Rotate
I have recently posted a much better implementation with completely revised multi-touch management:
http://developer.coronalabs.com/code/pinchzoom-made-real-easy
Previous example: https://developer.anscamobile.com/code/pinch-zoom-rotate
Full sample code: https://dl.dropbox.com/u/10254959/PinchZoomRotate.zip
[EDIT: If you take a closer look at the main.lua, I had recently added code to pinch-zoom a display group, instead of an image. This means that the group can have a lower quality image at smaller scales and a larger image (lazily loaded) at larger scales. While this could be handled in the pinch library, I thought it better - for now - to handle it in a more client-controlled manner.]
The previous pinch-zoom I developed was my first fully successful attempt to provide 2 finger pinch zooming. It works fine, but is limited by requiring the use of a display group (which effectively uses Corona's behind-the-scenes code to perform the tougher calculations) and can only handle two touch points.
This code is not limited in that way...
- It will handle any number of touch points
- Using it is simply a matter of calling one function (requires no setup)
- One touch point simply moves the image around
- Does not require you to store any separately stored state information (though it does store some state info on the image object itself, called __pinchzoomdata)
- Does not require any support display objects (all calculations are handle internally)
- Each pinch iteration is atomic and so does not require a stored value to be built, it only requires the previous and current touch point locations to work (this is all handled internally)
Other than this, the supporting math library is not tied in function to the pinch library, though the pinch lib does require the math lib.
The operations it performs are translation (moving relative to the touch points), scaling (pinch zooming relative to the touch points) and rotation (turning the image relative to the touch points.)
Basically, it works how you'd expect it to.
Currently, it does not provide the stretchy bounding limits which apps like Photos do. Also, you need to manage your own touch points and pass them in as a table. This is not difficult and often it is most easily managed by having a touch listener on the image to be manipulated and creating tracking display objects within a specially reserved display group.
Oh, as always you need to enable multitouch as each touch event needs an ID and you should provide an image where mine is titled "yoda.png" at the top of main.lua
So, here's the three files - main.lua, pinchlib.lua and mathlib.lua:
Please note: Multiple points may not work as expected if you have iOS Gestures or Accessibility switched on.
main.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | local pinchlibapi = require("pinchlib") display.setStatusBar( display.HiddenStatusBar ) system.activate( "multitouch" ) --[[ There is no reason that the device environment could use display objects and stage:setFocus to track touch events... ]]-- --[[ This section handles the simulator interaction which is performed by display objects representing touches. ]]-- local stage = display.getCurrentStage() -- use display group to allow image switching during scaling local img = display.newGroup() -- load smaller version of image img.x1 = display.newImage( img, "yoda.png" ) img.x1.x, img.x1.y = 0, 0 -- place image in centre of pinched group -- load larger version of image (this would normally be done lazily, to save memory) img.x2 = display.newImage( img, "scene.png" ) img.x2.x, img.x2.y = 0, 0 -- place image in centre of pinched group img.x2.xScale, img.x2.yScale = .5, .5 -- scale large version so it doesn't jump when coming into view img.x2.alpha = 0 -- hide the larger version img.x, img.y = display.contentCenterX, display.contentCenterY -- handles calling the pinch for simulator function simPinch() local points = {} for i=1, stage.numChildren do if (stage[i].name == "touchpoint") then points[#points+1] = stage[i] end end pinchlibapi.doPinchZoom( img, points ) -- handle the image switching if (img.xScale < 2) then img.x2.alpha = 0 -- hide the larger scale image if below 2x scale else img.x2.alpha = 1 -- show the higher quality image if scaled large enough end -- for simulator, print the scaling info print(img.xScale, img.yScale) end -- handles the simulator function tap(event) local circle = display.newCircle(event.x, event.y, 25) circle.name = "touchpoint" circle.id = system.getTimer() circle.strokeWidth = 2 circle:setStrokeColor(255,0,0) circle:setFillColor(0,0,255) circle.alpha = .6 circle:addEventListener("tap", circle) circle:addEventListener("touch", circle) function circle:tap(event) circle:removeEventListener("tap",self) circle:removeEventListener("touch",self) circle:removeSelf() -- reset pinch data to avoid jerking the image when the average centre suddenly moves simPinch() return true end function circle:touch(event) if (event.phase == "began") then stage:setFocus(circle) elseif (event.phase == "moved") then circle.x, circle.y = event.x, event.y elseif (event.phase == "ended" or event.phase == "cancelled") then circle.x, circle.y = event.x, event.y stage:setFocus(nil) end simPinch() return true end simPinch() return true end --[[ This section handles device interaction which simply holds a list of the current touch events. ]]-- local touches = {} -- handles calling the pinch for device function devPinch( event, remove ) -- look for event to update or remove for i=1, #touches do if (touches[i].id == event.id) then -- update the list of tracked touch events if (remove) then table.remove( touches, i ) else touches[i] = event end -- update the pinch pinchlibapi.doPinchZoom( img, touches ) return end end -- add unknown event to list touches[#touches+1] = event pinchlibapi.doPinchZoom( img, touches ) -- handle the image switching if (img.xScale < 2) then img.x2.alpha = 0 -- hide the larger scale image if below 2x scale else img.x2.alpha = 1 -- show the higher quality image if scaled large enough end end -- handles the device function touch(event) -- handle the touch if (event.phase == "began") then pinchlibapi.doPinchZoom( img,{} ) devPinch( event ) elseif (event.phase == "moved") then devPinch( event ) else pinchlibapi.doPinchZoom( img,{} ) devPinch( event, true ) end end --[[ This section attaches the appropriate touch/tap handler for the environment (simulator or device). ]]-- -- Please note that the XCode simulator will be handled as 'device' although it has no way to provide multitouch events. if (system.getInfo( "environment" ) == "simulator") then Runtime:addEventListener("tap",tap) -- mouse being used to create moveable touch avatars elseif (system.getInfo( "environment" ) == "device") then Runtime:addEventListener("touch",touch) -- fingers being used to create real touch events end |
pinchlib.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | module(..., package.seeall) local mathlibapi = require("mathlib") -- requires a collection of touch points -- each point must have '.id' to be tracked otherwise it will be ignored -- each point must be in world coordinates (default state of touch event coordinates) function doPinchZoom( img, points ) -- must have an image to manipulate if (not img) then return end -- is this the end of the pinch? if (not points or not img.__pinchzoomdata or #points ~= #img.__pinchzoomdata.points) then -- reset data (when #points changes) img.__pinchzoomdata = nil -- exit if there are no calculations to do if (not points or #points == 0) then return -- nothing to do end end -- get local ref to zoom data local olddata = img.__pinchzoomdata -- create newdata table local newdata = {} -- store img x,y in world coordinates newdata.imgpos = getImgPos( img ) -- calc centre (build list of points for later - avoids storing actual event objects passed in) newdata.centre, newdata.points = getCentrePoints( points ) -- calc distances and angles from centre point calcDistancesAndAngles( newdata ) -- does pinching need to be performed? if (olddata) then -- translation of centre newdata.imgpos.x = newdata.imgpos.x + newdata.centre.x - olddata.centre.x newdata.imgpos.y = newdata.imgpos.y + newdata.centre.y - olddata.centre.y -- get scaling factor and rotation difference if (#newdata.points > 1) then newdata.scalefactor, newdata.rotation = calcScaleAndRotation( olddata, newdata ) else newdata.scalefactor, newdata.rotation = 1, 0 end -- scale around pinch centre (translation) newdata.imgpos.x = newdata.centre.x + ((newdata.imgpos.x - newdata.centre.x) * newdata.scalefactor) newdata.imgpos.y = newdata.centre.y + ((newdata.imgpos.y - newdata.centre.y) * newdata.scalefactor) -- rotate around pinch centre newdata.imgpos = mathlibapi.rotateAboutPoint( newdata.imgpos, newdata.centre, newdata.rotation, false ) -- convert to local coordinates local x, y = img.parent:contentToLocal( newdata.imgpos.x, newdata.imgpos.y ) -- apply pinch... img.x, img.y = x, y img.rotation = img.rotation + newdata.rotation img.xScale, img.yScale = img.xScale * newdata.scalefactor, img.yScale * newdata.scalefactor end -- store new data img.__pinchzoomdata = newdata end -- simply converts the display object's centre x,y into world coordinates function getImgPos( img ) local x, y = img:localToContent( 0, 0 ) return { x=x, y=y } end -- calculates the centre of the points -- generates a new list of points so we are not storing the list of events from calling code function getCentrePoints( points ) local x, y = 0, 0 local newpoints = {} for i=1, #points do -- accumulate the centre values x = x + points[i].x y = y + points[i].y -- record the point with it's associated data newpoints[#newpoints+1] = { x=points[i].x, y=points[i].y, id=points[i].id } end -- return the list of points for next time and the centre point of this list return { x = x / #points, y = y / #points }, -- centre newpoints -- list of points end -- calculates the distance from the centre to each point and their angle if the centre is assumed to be 0,0 function calcDistancesAndAngles( data ) for i=1, #data.points do data.points[i].length = mathlibapi.lengthOf( data.centre, data.points[i] ) data.points[i].angle = mathlibapi.angleBetweenPoints( data.centre, data.points[i] ) end end -- calculates the change in scale between the old and new points -- also calculates the change in rotation around the centre point -- uses their average change function calcScaleAndRotation( olddata, newdata ) local scalediff, anglediff = 0, 0 for i=1, #newdata.points do local oldpoint = getPointById( newdata.points[i], olddata.points ) scalediff = scalediff + newdata.points[i].length / oldpoint.length anglediff = anglediff + mathlibapi.smallestAngleDiff(newdata.points[i].angle, oldpoint.angle) end return scalediff / #newdata.points, -- scale factor anglediff / #newdata.points -- rotation average end -- returns the newpoint if it does not have a previous version, or the old point if it has simply moved function getPointById( newpoint, points ) for i=1, #points do if (points[i].id == newpoint.id) then return points[i] end end return newpoint end |
mathlib.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | module(..., package.seeall) -- returns the distance between points a and b function lengthOf( a, b ) local width, height = b.x-a.x, b.y-a.y return math.sqrt(width*width + height*height) end -- converts degree value to radian value, useful for angle calculations function convertDegreesToRadians( degrees ) -- return (math.pi * degrees) / 180 return math.rad(degrees) end function convertRadiansToDegrees( radians ) return math.deg(radians) end -- rotates a point around the (0,0) point by degrees -- returns new point object function rotatePoint( point, degrees ) local x, y = point.x, point.y local theta = convertDegreesToRadians( degrees ) local pt = { x = x * math.cos(theta) - y * math.sin(theta), y = x * math.sin(theta) + y * math.cos(theta) } return pt end -- rotates point around the centre by degrees -- rounds the returned coordinates using math.round() if round == true -- returns new coordinates object function rotateAboutPoint( point, centre, degrees, round ) local pt = { x=point.x - centre.x, y=point.y - centre.y } pt = rotatePoint( pt, degrees ) pt.x, pt.y = pt.x + centre.x, pt.y + centre.y if (round) then pt.x = math.round(pt.x) pt.y = math.round(pt.y) end return pt end -- returns the degrees between (0,0) and pt -- note: 0 degrees is 'east' function angleOfPoint( pt ) local x, y = pt.x, pt.y local radian = math.atan2(y,x) --print('radian: '..radian) local angle = radian*180/math.pi --print('angle: '..angle) if angle < 0 then angle = 360 + angle end --print('final angle: '..angle) return angle end -- returns the degrees between two points -- note: 0 degrees is 'east' function angleBetweenPoints( a, b ) local x, y = b.x - a.x, b.y - a.y return angleOfPoint( { x=x, y=y } ) end -- Takes a centre point, internal point and radius of a circle and returns the location of the extruded point on the circumference -- In other words: Gives you the intersection between a line and a circle, if the line starts from the centre of the circle function calcCirclePoint( centre, point, radius ) local distance = lengthOf( centre, point ) local fraction = distance / radius local remainder = 1 - fraction local width, height = point.x - centre.x, point.y - centre.y local x, y = centre.x + width / fraction, centre.y + height / fraction local px, py = x - point.x, y - point.y return px, py end -- returns the smallest angle between the two angles -- ie: the difference between the two angles via the shortest distance function smallestAngleDiff( target, source ) local a = target - source if (a > 180) then a = a - 360 elseif (a < -180) then a = a + 360 end return a end |
I apologise for not having proper access to github right now!
- Type:
Replies
HOLY CRAPMONKEY!!!
This is so fricking COOOL and what a nice, elegant way to just drop in the actual functionality while the libraries take care of the heavy lifting behind the scenes!!!!!! I cannot tell you how awesome this is!!!!
Horace send me a paypal addy, I want to buy you a cup of coffee or a beer! No, seriously!! This is frickin' tits on a ritz!
Incredibly stoked,
Mario
Hey thanks - but really, just use it for good :)
Like a lot of people here, I enjoy writing useful, elegant pieces of code. If you can use it and it works well, then I'm happy and I'll write more.
I'm currently working on some physics code to help trajectory plotting etc, so keep your fingers crossed I can get this out - it's kicking my ass right now.
M
Ps: If you still want to buy me a coffee, check out "Tiltopolis" on the iOS App Store :)
Well, I finally got it - trajectory plotting code for an object fired using applyForce:
Works Great! Thanks
How to use it with director class? It work fine until you change screen with director.
In simulator:
Before changing screen : one tap = one point --no problem
After changing screen: one tap = 2 points --the problem
changing screen: one tap = 3 points
.....................and so on...........
Device:
Before changing screen: -- no problem
After changing screen: devPinch( event, true ) --not removing touches
I'm afraid I don't know - I've not used Director. I assume it would be something to do with the display groups used.
This is great. Works really well. Thanks for sharing!
You code libraries are super-awesome!!!!
Hats off!
Great job! I've found this very useful in the project I'm working on, I rewrote it so that the camera pans only when two fingers touch the screen and zooming is disabled when the fingers are within a certain range of each other. (disabled the rotation)
However, I've found that the game don't register finger presses that are too close together, in my case I can't keep my index and middle finger together when trying to pan the camera, it only works if I keep them separated (only a small amount is needed though). This is feels awkward though.
I had the same problem with the official multitouch template, have you encountered this or might it be specific to the phone I'm trying it on?
IMHO, If touches are too close, they will be registered as one touch. Just how close are your fingers in this situation?
I tried to build the code so that utilising devs could strip out parts they wanted to change, like you've removed the scaling for certain scenarios.
I see, that would definitely explain it. As close as they can be, like you would point towards something with two fingers or just the way it's most comfortable to swipe with two fingers.
It's great in that regard, being able to simulate multitouch is a lifesaver as well. Once again, thanks for a really great job!
Yep, I've noticed other apps registering close touches as one. Of course, from a logic point of view, you have to wonder that if someone has their fingers that close do they really intend to use them as one point effectively, or not...
Thanks a lot for your work!!! It's a very useful library.
Only comment a problem that I have. It works in the simulator, but when I generate an apk to check it in my device, it doesn't work properly. It makes some strange rotations and changes in the image. Do anyone this problem too??
Best regards and thanks again for your work! It's amazing!!
If you're using Director then it acts all whacky, which made me abandon this awesome, AWESOME piece of code. Something to do with the displaygroups that Director uses....now if you're not using Director, then.....Horace? You're up!
I use director.. And tweaked the code a bit to suit the needs.
It works pretty awesome. I even have an app in the store using a modified version of this code.
I just removed the
1 2 3 | local img = display.newGroup() img.x1 blah blah img.x2 blah blah |
stuff and just assigned
1 | img = object |
Only drawback is you cannot have 2 different images. Like the images used here; one for smaller zoom and one for higher zoom.
Dang, I tried but applied it to a displaygroup and it didn't register the "release" event and couldn't get around it, just like another user posted above. Ah well, I'm still doing fine with the other pinch zoom lib...I believe it was written by the same person!
Thanks for the support guys. I have not used the Director class as the Storyboard API came out just as I needed that type of functionality, however...
This touch API basically assumes that the image or display group you're working with is using the same coordinates that the touch events are working in, ie: world coordinates. This will cause some issues if you're using a display group manager which is playing by it's own rules. What you might want to do is translate the input touch values to content values before passing them to the doPinchZoom function.
Like I said, though, I can't guarantee that will work. Also, I've not tested this on Android, only iPhone, iPad and sim.
It's certainly for me the most usefull piece of code, thank you Horace.
Does anybody made the "double tap" zooming to a given scale factor ?
I believe the double tap zoom is context sensitive. What I mean is that when you double tap an image, the page is centred and zoomed to fit the image full screen.
A different application would have to use it's own logic there.
Thank you for your reply. I agree, but did you already create a function to "track" the double tap ? If yes is it available somewhere ?
Thank you in advance !
Oh, no, sorry. It would not be hard to do because the tap event contains a property 'numTaps'.
Thank you. The idea was to avoid re-develop something that already exists but you're right, with numTaps property we have just to adjust xScale and yScale to desired zoom and position the image correctly using tap coordinates.
One again thank you for you library.
Don't know if you just missed it or if I'm wrong but shouldn't
1 2 | -- apply pinch... img.x, img.y = x, y |
be
1 2 | -- apply pinch... img.x, img.y = img.x + x, img.y + y |
no - the coords have been converted to world coords by that time, using x and y as working variables
Hmm, guess I must be doing something wrong then. For me the only values x and y gets set to is either -1, 0 or 1.
What's the output of your code? Are you sure you're passing in appropriate values?
It turned out that depending on which group I passed the variables were different some went up to 300 but then reset to 0, this meant the camera wouldn't move but would snap back almost immediately after moving.
I'm pretty sure it has to do with the way I've structured my game, however, none of this matters as it works perfectly fine by adding the values to the existing imgpos.
Hi Horacebury,
I'm trying to remove the "rotation" but if I just remove the
img.rotation = img.rotation + newdata.rotation
in pinchlib library
further calculation are bad, the image is moving while moving the second touch point.
Help would be appreciated !
Thank in advance
JC
With the code below the sample image scales and moves without rotation just fine. Let me know if you have any problems. Ironically, the only code I've changed is to comment out the line you've commented out. Perhaps there are other changes you've made? I don't think I've changed anything else since I wrote this code.
Matt.
main.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | local pinchlibapi = require("pinchlib") display.setStatusBar( display.HiddenStatusBar ) system.activate( "multitouch" ) --[[ There is no reason that the device environment could use display objects and stage:setFocus to track touch events... ]]-- --[[ This section handles the simulator interaction which is performed by display objects representing touches. ]]-- local stage = display.getCurrentStage() -- use display group to allow image switching during scaling local img = display.newGroup() -- load smaller version of image img.x1 = display.newImage( img, "yoda.png" ) img.x1.x, img.x1.y = 0, 0 -- place image in centre of pinched group -- load larger version of image (this would normally be done lazily, to save memory) img.x2 = display.newImage( img, "scene.png" ) img.x2.x, img.x2.y = 0, 0 -- place image in centre of pinched group img.x2.xScale, img.x2.yScale = .5, .5 -- scale large version so it doesn't jump when coming into view img.x2.alpha = 0 -- hide the larger version img.x, img.y = display.contentCenterX, display.contentCenterY -- handles calling the pinch for simulator function simPinch() local points = {} for i=1, stage.numChildren do if (stage[i].name == "touchpoint") then points[#points+1] = stage[i] end end pinchlibapi.doPinchZoom( img, points ) -- handle the image switching if (img.xScale < 2) then img.x2.alpha = 0 -- hide the larger scale image if below 2x scale else img.x2.alpha = 1 -- show the higher quality image if scaled large enough end -- for simulator, print the scaling info print(img.xScale, img.yScale) end -- handles the simulator function tap(event) local circle = display.newCircle(event.x, event.y, 25) circle.name = "touchpoint" circle.id = system.getTimer() circle.strokeWidth = 2 circle:setStrokeColor(255,0,0) circle:setFillColor(0,0,255) circle.alpha = .6 circle:addEventListener("tap", circle) circle:addEventListener("touch", circle) function circle:tap(event) circle:removeEventListener("tap",self) circle:removeEventListener("touch",self) circle:removeSelf() -- reset pinch data to avoid jerking the image when the average centre suddenly moves simPinch() return true end function circle:touch(event) if (event.phase == "began") then stage:setFocus(circle) elseif (event.phase == "moved") then circle.x, circle.y = event.x, event.y elseif (event.phase == "ended" or event.phase == "cancelled") then circle.x, circle.y = event.x, event.y stage:setFocus(nil) end simPinch() return true end simPinch() return true end --[[ This section handles device interaction which simply holds a list of the current touch events. ]]-- local touches = {} -- handles calling the pinch for device function devPinch( event, remove ) -- look for event to update or remove for i=1, #touches do if (touches[i].id == event.id) then -- update the list of tracked touch events if (remove) then table.remove( touches, i ) else touches[i] = event end -- update the pinch pinchlibapi.doPinchZoom( img, touches ) return end end -- add unknown event to list touches[#touches+1] = event pinchlibapi.doPinchZoom( img, touches ) -- handle the image switching if (img.xScale < 2) then img.x2.alpha = 0 -- hide the larger scale image if below 2x scale else img.x2.alpha = 1 -- show the higher quality image if scaled large enough end end -- handles the device function touch(event) -- handle the touch if (event.phase == "began") then pinchlibapi.doPinchZoom( img,{} ) devPinch( event ) elseif (event.phase == "moved") then devPinch( event ) else pinchlibapi.doPinchZoom( img,{} ) devPinch( event, true ) end end --[[ This section attaches the appropriate touch/tap handler for the environment (simulator or device). ]]-- -- Please note that the XCode simulator will be handled as 'device' although it has no way to provide multitouch events. if (system.getInfo( "environment" ) == "simulator") then Runtime:addEventListener("tap",tap) -- mouse being used to create moveable touch avatars elseif (system.getInfo( "environment" ) == "device") then Runtime:addEventListener("touch",touch) -- fingers being used to create real touch events end |
pinchlib.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | module(..., package.seeall) local mathlibapi = require("mathlib") -- requires a collection of touch points -- each point must have '.id' to be tracked otherwise it will be ignored -- each point must be in world coordinates (default state of touch event coordinates) function doPinchZoom( img, points ) -- must have an image to manipulate if (not img) then return end -- is this the end of the pinch? if (not points or not img.__pinchzoomdata or #points ~= #img.__pinchzoomdata.points) then -- reset data (when #points changes) img.__pinchzoomdata = nil -- exit if there are no calculations to do if (not points or #points == 0) then return -- nothing to do end end -- get local ref to zoom data local olddata = img.__pinchzoomdata -- create newdata table local newdata = {} -- store img x,y in world coordinates newdata.imgpos = getImgPos( img ) -- calc centre (build list of points for later - avoids storing actual event objects passed in) newdata.centre, newdata.points = getCentrePoints( points ) -- calc distances and angles from centre point calcDistancesAndAngles( newdata ) -- does pinching need to be performed? if (olddata) then -- translation of centre newdata.imgpos.x = newdata.imgpos.x + newdata.centre.x - olddata.centre.x newdata.imgpos.y = newdata.imgpos.y + newdata.centre.y - olddata.centre.y -- get scaling factor and rotation difference if (#newdata.points > 1) then newdata.scalefactor, newdata.rotation = calcScaleAndRotation( olddata, newdata ) else newdata.scalefactor, newdata.rotation = 1, 0 end -- scale around pinch centre (translation) newdata.imgpos.x = newdata.centre.x + ((newdata.imgpos.x - newdata.centre.x) * newdata.scalefactor) newdata.imgpos.y = newdata.centre.y + ((newdata.imgpos.y - newdata.centre.y) * newdata.scalefactor) -- rotate around pinch centre newdata.imgpos = mathlibapi.rotateAboutPoint( newdata.imgpos, newdata.centre, newdata.rotation, false ) -- convert to local coordinates local x, y = img.parent:contentToLocal( newdata.imgpos.x, newdata.imgpos.y ) -- apply pinch... img.x, img.y = x, y --img.rotation = img.rotation + newdata.rotation img.xScale, img.yScale = img.xScale * newdata.scalefactor, img.yScale * newdata.scalefactor end -- store new data img.__pinchzoomdata = newdata end -- simply converts the display object's centre x,y into world coordinates function getImgPos( img ) local x, y = img:localToContent( 0, 0 ) return { x=x, y=y } end -- calculates the centre of the points -- generates a new list of points so we are not storing the list of events from calling code function getCentrePoints( points ) local x, y = 0, 0 local newpoints = {} for i=1, #points do -- accumulate the centre values x = x + points[i].x y = y + points[i].y -- record the point with it's associated data newpoints[#newpoints+1] = { x=points[i].x, y=points[i].y, id=points[i].id } end -- return the list of points for next time and the centre point of this list return { x = x / #points, y = y / #points }, -- centre newpoints -- list of points end -- calculates the distance from the centre to each point and their angle if the centre is assumed to be 0,0 function calcDistancesAndAngles( data ) for i=1, #data.points do data.points[i].length = mathlibapi.lengthOf( data.centre, data.points[i] ) data.points[i].angle = mathlibapi.angleBetweenPoints( data.centre, data.points[i] ) end end -- calculates the change in scale between the old and new points -- also calculates the change in rotation around the centre point -- uses their average change function calcScaleAndRotation( olddata, newdata ) local scalediff, anglediff = 0, 0 for i=1, #newdata.points do local oldpoint = getPointById( newdata.points[i], olddata.points ) scalediff = scalediff + newdata.points[i].length / oldpoint.length anglediff = anglediff + mathlibapi.smallestAngleDiff(newdata.points[i].angle, oldpoint.angle) end return scalediff / #newdata.points, -- scale factor anglediff / #newdata.points -- rotation average end -- returns the newpoint if it does not have a previous version, or the old point if it has simply moved function getPointById( newpoint, points ) for i=1, #points do if (points[i].id == newpoint.id) then return points[i] end end return newpoint end |
mathlib.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | module(..., package.seeall) -- returns the distance between points a and b function lengthOf( a, b ) local width, height = b.x-a.x, b.y-a.y return math.sqrt(width*width + height*height) end -- converts degree value to radian value, useful for angle calculations function convertDegreesToRadians( degrees ) -- return (math.pi * degrees) / 180 return math.rad(degrees) end function convertRadiansToDegrees( radians ) return math.deg(radians) end -- rotates a point around the (0,0) point by degrees -- returns new point object function rotatePoint( point, degrees ) local x, y = point.x, point.y local theta = convertDegreesToRadians( degrees ) local pt = { x = x * math.cos(theta) - y * math.sin(theta), y = x * math.sin(theta) + y * math.cos(theta) } return pt end -- rotates point around the centre by degrees -- rounds the returned coordinates using math.round() if round == true -- returns new coordinates object function rotateAboutPoint( point, centre, degrees, round ) local pt = { x=point.x - centre.x, y=point.y - centre.y } pt = rotatePoint( pt, degrees ) pt.x, pt.y = pt.x + centre.x, pt.y + centre.y if (round) then pt.x = math.round(pt.x) pt.y = math.round(pt.y) end return pt end -- returns the degrees between (0,0) and pt -- note: 0 degrees is 'east' function angleOfPoint( pt ) local x, y = pt.x, pt.y local radian = math.atan2(y,x) --print('radian: '..radian) local angle = radian*180/math.pi --print('angle: '..angle) if angle < 0 then angle = 360 + angle end --print('final angle: '..angle) return angle end -- returns the degrees between two points -- note: 0 degrees is 'east' function angleBetweenPoints( a, b ) local x, y = b.x - a.x, b.y - a.y return angleOfPoint( { x=x, y=y } ) end -- Takes a centre point, internal point and radius of a circle and returns the location of the extruded point on the circumference -- In other words: Gives you the intersection between a line and a circle, if the line starts from the centre of the circle function calcCirclePoint( centre, point, radius ) local distance = lengthOf( centre, point ) local fraction = distance / radius local remainder = 1 - fraction local width, height = point.x - centre.x, point.y - centre.y local x, y = centre.x + width / fraction, centre.y + height / fraction local px, py = x - point.x, y - point.y return px, py end -- returns the smallest angle between the two angles -- ie: the difference between the two angles via the shortest distance function smallestAngleDiff( target, source ) local a = target - source if (a > 180) then a = a - 360 elseif (a < -180) then a = a + 360 end return a end -- is clockwise is false this returns the shortest angle between the points --[[ function AngleDiff( pointA, pointB, clockwise ) local angleA, angleB = AngleOfPoint( pointA ), AngleOfPoint( pointB ) if angleA == angleB then return 0 end if clockwise then if angleA > angleB then return angleA - angleB else return 360 - (angleB - angleA) end else if angleA > angleB then return angleB + (360 - angleA) else return angleB - angleA end end end ]]-- -- test code... --[[ local pointA = { x=10, y=-10 } -- anticlockwise 45 deg from east local pointB = { x=-10, y=-10 } -- clockwise 45 deg from east print('Angle of point A: '..tostring(AngleOfPoint( pointA ))) print('Angle of point B: '..tostring(AngleOfPoint( pointB ))) print('Clockwise: '..tostring(AngleDiff(pointA,pointB,true))) print('Anti-Clockwise: '..tostring(AngleDiff(pointA,pointB,false))) ]]-- |
Thank you so much, it's OK, I will take a closer look inthe afternoon but I made a quick test and it's semmes perfectly what I expected.
Any way to help you buy your next iPad or whatever ?
Oooops, after a deeper test it doesn'work, sorry ! Just create a first tap circle to the left of the screen, a second one near the center and love it, you'll see that the image is moving down up and left or right depending the initial position of the first tap point.
It seems OK when you use the center as initial tap
In the main.lua if you record the rotation value of the image prior to applying the pinch zoom as normal, then set it back after the effect, you'll notice the same effect happening. I believe that this is because the rotation of the image is essentially part of the scaling and translation of the image, which means that to have the image perform all the normal pinch zoom operations while maintaining a fixed rotation would require some extra work.
The logic I've used in this implementation is to calculate the pinch operation upon a logical element set directly between the touch points. This is then translated into world coordinates and applied to the point on the image which falls directly between the touches. The effectively applies the scale, rotate and translate to the point between the touches on the image, rather than the regular point on the image where those would be applied: the centre or reference point.
I will see if I can apply a little extra work to make this happen, but if you have any success getting round it, please let me know.
Matt.
It's about 9 AM here (France) and I've decided to spend my sunday morning working the topic. I will let you know.Thx.
EDIR: the same, one day later, ifx not found.
The solution is to make the image zooming while keeping the image x y that is under the FIRST touch fix but I've not found the right formula.
Now I understand that I lack somthing not being concentrated during math class :o)
I'll let you know what I find - though my time is not my own at present!
Same problem :o( I've tried to fiind other pinch/zoom libraries but not fpund any better than yours. It would be nice to have it as basic function included in SDK
I'm a bit lost in coordinates, but I'm confident in finding. Anyway help is appreciated, thankypu Matt.
Thanks :)
It's a funny situation with pinching because you have to do so much to get it right and changing on small parameter makes everything completely different. Throw in the complexity of not really knowing what it is which is being manipulated and it can mess with your mind.
Now this code has been here a while and the concept is fairly cemented in my mind, I'll see if I can re-work it to be a bit more straightforward.
Ohhh yes. Actually I think that the patch is to get the angle between horizontal line and the diagonal between first touch and center of the zoomed object.
Then adjust img.x and img.y when zooming in order to keep this angle constant.
Something to see with Pythagore and sinus or cosinus, not really friends of mine :o)
Well, the logic I use is this:

The red circles are touch points. These are created by a touch on the image but all following touch events are focussed on those circles, implemented as invisible display objects with setFocus.
The manipulation of the centre point between the touch points happens and then the line from the touch centre point to the centre of the actual image (or display group) is scaled toward the touch centre. The centre of the actual image is rotated around the touch centre. Scaling is applied to the image as well as the line connecting the two centres, to calculate the position of the image.
May be the simple way would be to test if fingers have made a rotation and to disable zooming when angle have varied of more than some degrees ??
Just wanted to let you know that I've updated the pinch zoom to include rotation suppression. The pinchlib doPinchZoom now takes the parameter 'suppressRotation' which, if true, stops rotation.
main and pinchlib have both been updated, as the demo main calls doPinchZoom in four different places...
main.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | local pinchlibapi = require("pinchlib") display.setStatusBar( display.HiddenStatusBar ) system.activate( "multitouch" ) --[[ There is no reason that the device environment could use display objects and stage:setFocus to track touch events... ]]-- --[[ This section handles the simulator interaction which is performed by display objects representing touches. ]]-- local suppressrotation = true local stage = display.getCurrentStage() local img = display.newImage( "yoda.png" ) img.x, img.y = display.contentCenterX, display.contentCenterY -- handles calling the pinch for simulator function simPinch() local points = {} for i=1, stage.numChildren do if (stage[i].name == "touchpoint") then points[#points+1] = stage[i] end end pinchlibapi.doPinchZoom( img, points, suppressrotation ) end -- handles the simulator function tap(event) local circle = display.newCircle(event.x, event.y, 25) circle.name = "touchpoint" circle.id = system.getTimer() circle.strokeWidth = 2 circle:setStrokeColor(255,0,0) circle:setFillColor(0,0,255) circle.alpha = .6 circle:addEventListener("tap", circle) circle:addEventListener("touch", circle) function circle:tap(event) circle:removeEventListener("tap",self) circle:removeEventListener("touch",self) circle:removeSelf() -- reset pinch data to avoid jerking the image when the average centre suddenly moves simPinch() return true end function circle:touch(event) if (event.phase == "began") then stage:setFocus(circle) elseif (event.phase == "moved") then circle.x, circle.y = event.x, event.y elseif (event.phase == "ended" or event.phase == "cancelled") then circle.x, circle.y = event.x, event.y stage:setFocus(nil) end simPinch() return true end simPinch() return true end --[[ This section handles device interaction which simply holds a list of the current touch events. ]]-- local touches = {} -- handles calling the pinch for device function devPinch( event, remove ) -- look for event to update or remove for i=1, #touches do if (touches[i].id == event.id) then -- update the list of tracked touch events if (remove) then table.remove( touches, i ) else touches[i] = event end -- update the pinch pinchlibapi.doPinchZoom( img, touches, suppressrotation ) return end end -- add unknown event to list touches[#touches+1] = event pinchlibapi.doPinchZoom( img, touches, suppressrotation ) end -- handles the device function touch(event) if (event.phase == "began") then pinchlibapi.doPinchZoom( img,{}, suppressrotation ) devPinch( event ) elseif (event.phase == "moved") then devPinch( event ) else pinchlibapi.doPinchZoom( img,{}, suppressrotation ) devPinch( event, true ) end end --[[ This section attaches the appropriate touch/tap handler for the environment (simulator or device). ]]-- -- Please note that the XCode simulator will be handled as 'device' although it has no way to provide multitouch events. if (system.getInfo( "environment" ) == "simulator") then Runtime:addEventListener("tap",tap) -- mouse being used to create moveable touch avatars elseif (system.getInfo( "environment" ) == "device") then Runtime:addEventListener("touch",touch) -- fingers being used to create real touch events end |
pinchlib.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | module(..., package.seeall) local mathlibapi = require("mathlib") -- requires a collection of touch points -- each point must have '.id' to be tracked otherwise it will be ignored -- each point must be in world coordinates (default state of touch event coordinates) function doPinchZoom( img, points, suppressRotation ) -- must have an image to manipulate if (not img) then return end -- is this the end of the pinch? if (not points or not img.__pinchzoomdata or #points ~= #img.__pinchzoomdata.points) then -- reset data (when #points changes) img.__pinchzoomdata = nil -- exit if there are no calculations to do if (not points or #points == 0) then return -- nothing to do end end -- get local ref to zoom data local olddata = img.__pinchzoomdata -- create newdata table local newdata = {} -- store img x,y in world coordinates newdata.imgpos = getImgPos( img ) -- calc centre (build list of points for later - avoids storing actual event objects passed in) newdata.centre, newdata.points = getCentrePoints( points ) -- calc distances and angles from centre point calcDistancesAndAngles( newdata ) -- does pinching need to be performed? if (olddata) then -- translation of centre newdata.imgpos.x = newdata.imgpos.x + newdata.centre.x - olddata.centre.x newdata.imgpos.y = newdata.imgpos.y + newdata.centre.y - olddata.centre.y -- get scaling factor and rotation difference if (#newdata.points > 1) then newdata.scalefactor, newdata.rotation = calcScaleAndRotation( olddata, newdata ) else newdata.scalefactor, newdata.rotation = 1, 0 end -- scale around pinch centre (translation) newdata.imgpos.x = newdata.centre.x + ((newdata.imgpos.x - newdata.centre.x) * newdata.scalefactor) newdata.imgpos.y = newdata.centre.y + ((newdata.imgpos.y - newdata.centre.y) * newdata.scalefactor) -- rotate around pinch centre if (suppressRotation) then newdata.rotation = 0 end newdata.imgpos = mathlibapi.rotateAboutPoint( newdata.imgpos, newdata.centre, newdata.rotation, false ) -- convert to local coordinates local x, y = img.parent:contentToLocal( newdata.imgpos.x, newdata.imgpos.y ) -- apply pinch... img.x, img.y = x, y img.rotation = img.rotation + newdata.rotation img.xScale, img.yScale = img.xScale * newdata.scalefactor, img.yScale * newdata.scalefactor end -- store new data img.__pinchzoomdata = newdata end -- simply converts the display object's centre x,y into world coordinates function getImgPos( img ) local x, y = img:localToContent( 0, 0 ) return { x=x, y=y } end -- calculates the centre of the points -- generates a new list of points so we are not storing the list of events from calling code function getCentrePoints( points ) local x, y = 0, 0 local newpoints = {} for i=1, #points do -- accumulate the centre values x = x + points[i].x y = y + points[i].y -- record the point with it's associated data newpoints[#newpoints+1] = { x=points[i].x, y=points[i].y, id=points[i].id } end -- return the list of points for next time and the centre point of this list return { x = x / #points, y = y / #points }, -- centre newpoints -- list of points end -- calculates the distance from the centre to each point and their angle if the centre is assumed to be 0,0 function calcDistancesAndAngles( data ) for i=1, #data.points do data.points[i].length = mathlibapi.lengthOf( data.centre, data.points[i] ) data.points[i].angle = mathlibapi.angleBetweenPoints( data.centre, data.points[i] ) end end -- calculates the change in scale between the old and new points -- also calculates the change in rotation around the centre point -- uses their average change function calcScaleAndRotation( olddata, newdata ) local scalediff, anglediff = 0, 0 for i=1, #newdata.points do local oldpoint = getPointById( newdata.points[i], olddata.points ) scalediff = scalediff + newdata.points[i].length / oldpoint.length anglediff = anglediff + mathlibapi.smallestAngleDiff(newdata.points[i].angle, oldpoint.angle) end return scalediff / #newdata.points, -- scale factor anglediff / #newdata.points -- rotation average end -- returns the newpoint if it does not have a previous version, or the old point if it has simply moved function getPointById( newpoint, points ) for i=1, #points do if (points[i].id == newpoint.id) then return points[i] end end return newpoint end |
Hi Matt,
Made a test on the device, it seems OK with your demo, once again thank you for your efforts improving your code.
Checking in a my app and will post results :o)
No worries. It occurred to me earlier that translation and scaling without rotation, the way you want it, was a side-effect I had seen during my initial development and I had not noted it properly.
Just want to post another update: Now the library supports suppressing scaling as well. Just pass a fourth bool value into doPinchZoom...
main.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | local pinchlibapi = require("pinchlib") display.setStatusBar( display.HiddenStatusBar ) system.activate( "multitouch" ) --[[ There is no reason that the device environment could use display objects and stage:setFocus to track touch events... ]]-- --[[ This section handles the simulator interaction which is performed by display objects representing touches. ]]-- local suppressrotation = true local suppressscaling = true local stage = display.getCurrentStage() local img = display.newImage( "yoda.png" ) img.x, img.y = display.contentCenterX, display.contentCenterY -- handles calling the pinch for simulator function simPinch() local points = {} for i=1, stage.numChildren do if (stage[i].name == "touchpoint") then points[#points+1] = stage[i] end end pinchlibapi.doPinchZoom( img, points, suppressrotation, suppressscaling ) end -- handles the simulator function tap(event) local circle = display.newCircle(event.x, event.y, 25) circle.name = "touchpoint" circle.id = system.getTimer() circle.strokeWidth = 2 circle:setStrokeColor(255,0,0) circle:setFillColor(0,0,255) circle.alpha = .6 circle:addEventListener("tap", circle) circle:addEventListener("touch", circle) function circle:tap(event) circle:removeEventListener("tap",self) circle:removeEventListener("touch",self) circle:removeSelf() -- reset pinch data to avoid jerking the image when the average centre suddenly moves simPinch() return true end function circle:touch(event) if (event.phase == "began") then stage:setFocus(circle) elseif (event.phase == "moved") then circle.x, circle.y = event.x, event.y elseif (event.phase == "ended" or event.phase == "cancelled") then circle.x, circle.y = event.x, event.y stage:setFocus(nil) end simPinch() return true end simPinch() return true end --[[ This section handles device interaction which simply holds a list of the current touch events. ]]-- local touches = {} -- handles calling the pinch for device function devPinch( event, remove ) -- look for event to update or remove for i=1, #touches do if (touches[i].id == event.id) then -- update the list of tracked touch events if (remove) then table.remove( touches, i ) else touches[i] = event end -- update the pinch pinchlibapi.doPinchZoom( img, touches, suppressrotation, suppressscaling ) return end end -- add unknown event to list touches[#touches+1] = event pinchlibapi.doPinchZoom( img, touches, suppressrotation, suppressscaling ) end -- handles the device function touch(event) if (event.phase == "began") then pinchlibapi.doPinchZoom( img,{}, suppressrotation, suppressscaling ) devPinch( event ) elseif (event.phase == "moved") then devPinch( event ) else pinchlibapi.doPinchZoom( img,{}, suppressrotation, suppressscaling ) devPinch( event, true ) end end --[[ This section attaches the appropriate touch/tap handler for the environment (simulator or device). ]]-- -- Please note that the XCode simulator will be handled as 'device' although it has no way to provide multitouch events. if (system.getInfo( "environment" ) == "simulator") then Runtime:addEventListener("tap",tap) -- mouse being used to create moveable touch avatars elseif (system.getInfo( "environment" ) == "device") then Runtime:addEventListener("touch",touch) -- fingers being used to create real touch events end |
pinchlib.lua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | module(..., package.seeall) local mathlibapi = require("mathlib") -- requires a collection of touch points -- each point must have '.id' to be tracked otherwise it will be ignored -- each point must be in world coordinates (default state of touch event coordinates) function doPinchZoom( img, points, suppressRotation, suppressScaling ) -- must have an image to manipulate if (not img) then return end -- is this the end of the pinch? if (not points or not img.__pinchzoomdata or #points ~= #img.__pinchzoomdata.points) then -- reset data (when #points changes) img.__pinchzoomdata = nil -- exit if there are no calculations to do if (not points or #points == 0) then return -- nothing to do end end -- get local ref to zoom data local olddata = img.__pinchzoomdata -- create newdata table local newdata = {} -- store img x,y in world coordinates newdata.imgpos = getImgPos( img ) -- calc centre (build list of points for later - avoids storing actual event objects passed in) newdata.centre, newdata.points = getCentrePoints( points ) -- calc distances and angles from centre point calcDistancesAndAngles( newdata ) -- does pinching need to be performed? if (olddata) then -- translation of centre newdata.imgpos.x = newdata.imgpos.x + newdata.centre.x - olddata.centre.x newdata.imgpos.y = newdata.imgpos.y + newdata.centre.y - olddata.centre.y -- get scaling factor and rotation difference if (#newdata.points > 1) then newdata.scalefactor, newdata.rotation = calcScaleAndRotation( olddata, newdata ) if (suppressScaling) then newdata.scalefactor = 1 end if (suppressRotation) then newdata.rotation = 0 end else newdata.scalefactor, newdata.rotation = 1, 0 end -- scale around pinch centre (translation) newdata.imgpos.x = newdata.centre.x + ((newdata.imgpos.x - newdata.centre.x) * newdata.scalefactor) newdata.imgpos.y = newdata.centre.y + ((newdata.imgpos.y - newdata.centre.y) * newdata.scalefactor) -- rotate around pinch centre newdata.imgpos = mathlibapi.rotateAboutPoint( newdata.imgpos, newdata.centre, newdata.rotation, false ) -- convert to local coordinates local x, y = img.parent:contentToLocal( newdata.imgpos.x, newdata.imgpos.y ) -- apply pinch... img.x, img.y = x, y img.rotation = img.rotation + newdata.rotation img.xScale, img.yScale = img.xScale * newdata.scalefactor, img.yScale * newdata.scalefactor end -- store new data img.__pinchzoomdata = newdata end -- simply converts the display object's centre x,y into world coordinates function getImgPos( img ) local x, y = img:localToContent( 0, 0 ) return { x=x, y=y } end -- calculates the centre of the points -- generates a new list of points so we are not storing the list of events from calling code function getCentrePoints( points ) local x, y = 0, 0 local newpoints = {} for i=1, #points do -- accumulate the centre values x = x + points[i].x y = y + points[i].y -- record the point with it's associated data newpoints[#newpoints+1] = { x=points[i].x, y=points[i].y, id=points[i].id } end -- return the list of points for next time and the centre point of this list return { x = x / #points, y = y / #points }, -- centre newpoints -- list of points end -- calculates the distance from the centre to each point and their angle if the centre is assumed to be 0,0 function calcDistancesAndAngles( data ) for i=1, #data.points do data.points[i].length = mathlibapi.lengthOf( data.centre, data.points[i] ) data.points[i].angle = mathlibapi.angleBetweenPoints( data.centre, data.points[i] ) end end -- calculates the change in scale between the old and new points -- also calculates the change in rotation around the centre point -- uses their average change function calcScaleAndRotation( olddata, newdata ) local scalediff, anglediff = 0, 0 for i=1, #newdata.points do local oldpoint = getPointById( newdata.points[i], olddata.points ) scalediff = scalediff + newdata.points[i].length / oldpoint.length anglediff = anglediff + mathlibapi.smallestAngleDiff(newdata.points[i].angle, oldpoint.angle) end return scalediff / #newdata.points, -- scale factor anglediff / #newdata.points -- rotation average end -- returns the newpoint if it does not have a previous version, or the old point if it has simply moved function getPointById( newpoint, points ) for i=1, #points do if (points[i].id == newpoint.id) then return points[i] end end return newpoint end |
@horace
Is there a way to use this library without having 2 to load both a "small" image and a "large" image? I plan to limit the scale sizing and don't want to use 2 copies of each image. Also I don't want to use a full display group, I want to use this code on individual images. (doing a sticker mode type thing) Would I just use event.target rather than "img"?
Definitely. The two images are handled outside of the pinch lib so just use one in the display group. If you don't want a display group at all just pass in an image. The group in the code above is actually called 'img', just replace that with a real image.
Nice Horacebury!
@horacebury,
Thanks again for your help. I've spent the last couple days messing with this code to try and get it working as I'd like. Is there any chance that you could help me out? I would like to be able to add an event listener to any image (not group) and have it work with this code, I am having a hard time accomplishing this. Also I do not wish to have 2 images (small and large), just a single display object would have an event listener that calls this code and the user can manipulate the display object as the code allows already.
I would really really appreciate it if you could make an example project using your code with the modifications I described above. If not that is fine, I am sure you are very busy. Thanks
To be honest, I'm not really sure what you're asking for. The current version of the code does only have a single display object, in this case an image, and it does have an event listener, to control the user input of the multi-point touch. Any other listeners are not really relevant because this only needs touch events. If you want to be attaching your own touch listeners, that's fine - just follow the simulator code (function simPinch) to see how I've used display objects on the screen to simulate multiple touches on a device.
Can you maybe rethink or rephrase the requirements? Perhaps there is a part of my main.lua which you can use to your own ends.
I don't have any problem creating a sample project :) I don't use Project Manager though, I'm afraid.
Matt.
Btw, here's a link to the zip file of the complete solution:








Awesome work. I don't have a need for this right now, but I was curious. So, I loaded up the 3 lua files and put a graphic in. It worked like a charm. Thanks for sharing.