Share Your Code

Multi-Point Pinch Zoom Rotate

Posted by horacebury, Posted on December 2, 2011, Last updated November 14, 2012

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!


Replies

flyingaudio
User offline. Last seen 31 weeks 6 days ago. Offline
Joined: 24 Mar 2011

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.

mroberti
User offline. Last seen 1 hour 54 min ago. Offline
Joined: 20 Nov 2010

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

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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 :)

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

Well, I finally got it - trajectory plotting code for an object fired using applyForce:

http://developer.anscamobile.com/code/trajectory-plotting

me250
User offline. Last seen 45 weeks 15 hours ago. Offline
Joined: 19 Dec 2011

Works Great! Thanks

nta84
User offline. Last seen 7 hours 40 min ago. Offline
Joined: 13 Dec 2011

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

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

ec2
User offline. Last seen 5 days 3 hours ago. Offline
Joined: 27 Jan 2011

This is great. Works really well. Thanks for sharing!

Satheesh
User offline. Last seen 4 hours 38 min ago. Offline
Joined: 25 May 2011

You code libraries are super-awesome!!!!
Hats off!

info583
User offline. Last seen 1 year 22 weeks ago. Offline
Joined: 23 Feb 2012

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?

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

info583
User offline. Last seen 1 year 22 weeks ago. Offline
Joined: 23 Feb 2012

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!

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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...

tmapps
User offline. Last seen 1 week 20 hours ago. Offline
Joined: 9 Feb 2011

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!!

mroberti
User offline. Last seen 1 hour 54 min ago. Offline
Joined: 20 Nov 2010

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!

Satheesh
User offline. Last seen 4 hours 38 min ago. Offline
Joined: 25 May 2011

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
where object is the display object which I want to transform.

Only drawback is you cannot have 2 different images. Like the images used here; one for smaller zoom and one for higher zoom.

mroberti
User offline. Last seen 1 hour 54 min ago. Offline
Joined: 20 Nov 2010

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!

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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 ?

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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 !

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

Oh, no, sorry. It would not be hard to do because the tap event contains a property 'numTaps'.

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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.

info583
User offline. Last seen 1 year 22 weeks ago. Offline
Joined: 23 Feb 2012

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 

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

no - the coords have been converted to world coords by that time, using x and y as working variables

info583
User offline. Last seen 1 year 22 weeks ago. Offline
Joined: 23 Feb 2012

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.

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

What's the output of your code? Are you sure you're passing in appropriate values?

info583
User offline. Last seen 1 year 22 weeks ago. Offline
Joined: 23 Feb 2012

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.

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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)))
]]--

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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 ?

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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)

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

I'll let you know what I find - though my time is not my own at present!

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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.

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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)

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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 ??

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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

jch_apple
User offline. Last seen 4 hours 3 min ago. Offline
Joined: 4 Apr 2010

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)

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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

rxmarccall
User offline. Last seen 6 days 14 hours ago. Offline
Joined: 18 Jan 2011

@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"?

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

lessmsios
User offline. Last seen 16 hours 54 min ago. Offline
Joined: 11 Jun 2012

Nice Horacebury!

rxmarccall
User offline. Last seen 6 days 14 hours ago. Offline
Joined: 18 Jan 2011

@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

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

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.

horacebury
User offline. Last seen 2 hours 24 min ago. Offline
Joined: 17 Aug 2010

Btw, here's a link to the zip file of the complete solution:

https://dl.dropbox.com/u/10254959/PinchZoomRotate.zip