Posted on by

Today’s guest tutorial comes to you courtesy of Matt Webster, a.k.a. “HoraceBury.” Matt is a Development Manager working in central London. He has 15 years of experience building enterprise sites in .NET and Java, but he prefers Corona to build games and physics-based apps. As a Corona Labs Ambassador, Matt has organized two London meet-ups and looks forward to doing more in 2013. Matt’s first game in Corona was Tiltopolis, a hybrid twist on the classics Columns and Tetris.

Preface

First, please download the project files so you can follow along with the code samples. Each of the “sampleX” modules are functioning mini-projects and should be worked through one at a time within main.lua. Uncomment only one require statement at a time to follow the workings in the logic. The last module, sample11.lua, is the entire pinch-zoom-rotate module which you can incorporate into your own project. Enjoy!

Introduction

Most applications (more than you’d expect) can perform perfectly fine with just one touch point. If you consider the large number of apps out there, you can see that many have a huge feature set but still get by with just a single point of input because they are designed around buttons or individual swipe actions, etc.

Take “Angry Birds,” for example. This game requires that every tap, drag, and swipe is performed by one finger. Navigating the menu, opening up settings, and firing the afore-mentioned birds with attitude is all done with one finger, and rightly so. It makes for a simple, intuitive and engrossing game. However, even this most basic interface requires one simple trick learned from iOS. This involves using two fingers to “pinch” zoom in and out of the parallax-scrolling action.

So, that’s simple, isn’t it? The rule is: when one finger is used, perform the action for the object being touched. When two fingers are used, perform a gentle scaling of the top-level parent display group.

This tutorial aims to show you how to handle these multitouch scenarios with as little hassle as possible. It will also try to provide some insight into the oft-requested pinch zoom.

Touch Basics

If you’re reading this tutorial, you probably already have some experience with the Corona touch model, so I will just highlight the core tenets.

  • addEventListener() is used to listen to a particular display object for user touches.
  • There are two types for touch events: touch and tap.
  • The touch event is comprised of phases: began, moved and ended.
  • Listening to one display object for both touch and tap events will fire the touch event phases before the tap event fires.
  • Returning true from an event function stops Corona from passing that event to any display objects beneath the object.
  • system.activate(“multitouch”) enables multitouch.
  • Once a touch event has begun, future touch phases are directed to the same listener by calling display.getCurrentStage():setFocus().
  • setFocus can only be called once per object per event (without cancellation).
  • Calling dispatchEvent() on display objects fires artificial events.
  • Events fired with dispatchEvent do not propagate down the display hierarchy.

The Tap Problem

As described above, touch events have a number of phases which literally describe the users interaction with the device: putting the finger on the screen, moving it around, and letting go.

When it is listened for, the normal tap event is fired if the above event phases occur within a given time span — iOS employs about 350 milliseconds — and with a distance of less than ~10 pixels between the began and ended locations.

This means that if you are listening for both touch and tap events you need to actually detect a tap within your touch listener function to know that your tap listener function is going to be called. So, if you’re already detecting taps you may as well not attach a tap listener at all. For the purposes of this tutorial that’s exactly what we’ll do: we will leave out tap events because they simply complicate our code.

Single Touch

To demonstrate the typical touch event, let’s create a display object with a standard touch listener and use it to move the display object around.

sample1.lua

The above function handles touch events when multitouch is not activated. This isn’t the “simplest” touch listener, but it’s practical and safe. It’s also not the most complex that we could build, but any other work it can perform should be done by functions it can call. It caters for the following situations:

  • The touch starts on the object.
  • The touch is used to move the object.
  • Touches which start “off” the object are ignored.
  • Handled touches do not get passed to other display objects.
  • Ignored touches get propagated to other display objects.
  • The display object has its own :touch(e) function, not a global function.

Note that the object will ignore touches which start elsewhere. This is because setting hasFocus indicates that the object should accept touch phases after began. Also, it will not lose the touch once it acquires it because setFocus tells Corona to direct all further input to this object.

Multiple Touches

Fortunately, converting this function to be used by multiple display objects is not difficult. The catch with setFocus is that each display object can only listen for one touch because all other touch events are ignored on that object after it begins handling a touch.

To demonstrate multitouch we will convert the above code to create multiple objects which will handle one touch each.

sample2.lua

Note the key differences in this code:

  • We have activated multitouch.
  • We have wrapped the display object creation so that it can be called repeatedly.
  • setFocus accepts a specific touch ID to differentiate between user screen contacts.
  • When ending the touch, setFocus accepts nil to release the object’s touch input.

With the code above, we should be able to create 5 large circles, each of which can be moved independently. Note that, as before, due to setting hasFocus, and with setFocus now accepting a specific touch ID, the display objects will ignore touches which start elsewhere and they will not lose a touch once it begins.

The Multitouch Problem

Remember that the strength of the code above is that it can distinguish between multiple touches easily. This is because objects will not lose their touch once they acquire it. This is both a huge bonus and a bit of a problem.

  • The bonus is that setFocus allows us to say, “Send every move this user’s touch makes to my object’s event listener and nowhere else.”
  • The slight problem is that setFocus also stops our display object from receiving any other touch events.

If we have not yet called setFocus, using hasFocus conveniently allows our object to ignore touches which don’t begin there. This is useful because users often make a swiping gesture (by accident) on the background or inactive part of the screen and swipe across our object. We want it to ignore touches which don’t begin on it. So, the question is “how do we convince Corona to let our objects receive multiple touches?” when the functions which give us this great ease-of-use stop exactly that? The answer is to create a tracking object in the began phase.

The Concept

With a small change to the code above, we can create a single object which spawns multiple objects in its began phase. These objects will then track each touch individually. We will also change the code further to remove the tracking object when the touch ends. The complete code will have one function to listen for the touch event began phase and another to listen for moved, ended and cancelled phases. These two functions will be added to the target listening object and the tracking dot objects, respectively.

Spawning Tracking Dots

First, we need to create an object which will handle the began phase as before, but this time it will call a function to create a tracking dot.

sample3.lua

This is pretty straightforward. It just creates a display object which listens for the began phase of any unhandled touch events. When it receives a touch with a began phase, it calls the function which will create a new display object. This new object will be able to track the touch by directing the future touch phases to itself (instead of “rect”) by calling setFocus. Note that we are not setting the hasFocus value because multitouch objects only need to handle the began phase.

Next, we need to create the tracking dot. This code is almost identical to the previous multitouch function.

Note that the only two changes we’ve made to this function are:

  • We call circle:touch(e) because the circle has only been created and has not actually received the touch event’s began phase. Calling this allows the circle object to take control of the touch event away from the “rect” object and handle all future touch phases.
  • At the start of the :touch() function we also change to using the circle as the target because the e.target property is actually the “rect” object (where the touch began).

When this code is used with the code above we will see a small blue rectangle which can create multiple white circles. Each circle is moved by an independent touch. It is this mechanism which we can use to direct all of the touch information to our blue “rect” and pretend that it is receiving multitouch input.

Faking Multitouch Input

Our blue “rect” object is going to become the recipient of multiple touch inputs. To do this we need to first modify its touch listener function. At first we will simply add some print() statements for the moved, ended and cancelled phases. Here is the modified :touch() listener function for the small blue rectangle:

sample4.lua

30 Responses to “Tutorial: Implementing Pinch-Zoom-Rotate”

  1. Thomas Vanden Abeele

    Thank you for this extensive and very well laid out tutorial. It’s been WAY too long (since Jon left, I think) since the last time we had a tutorial of substance in my opinion, so this is nice to see and read.

    Brent, if I could make a request: what a lot of people in the community really need is a definitive guide on object oriented programming in Lua, because no matter how hard you search online, documentation on this is either sorely lacking, or thoroughly confusing “because there are many ways to do OOP in Lua”. If there are many ways, then please give us a full and complete description on how to do them all!

    I can practically guarantee you that a lot of Lua programmers will Google their way to Corona if you would have this content on offer with the same in-depth style as this tutorial.

    Thanks!

    Reply
    • Brent

      Hi Thomas,
      Thanks for the feedback. Rob and I try to mix it up when thinking up subject matter for the weekly tutorials, in order to appeal to the overall community. We generally focus on the most-requested features or “tricks” that aren’t necessarily obvious from just reading through documentation.

      That being said, I fully agree that a “proper” OOP tutorial would be useful… actually, well beyond useful! But as you’ve seen, nobody can seem to agree on what “proper” means when it comes to an OOP-type methodology in Lua.

      You’ve prompted me to look harder though… to “solve” this issue in a cohesive, well-documented manner would be awesome. ;)

      Reply
  2. russell

    This will be excellent tool for point an click adventures or hidden object type games. I suppose you will have to use a higher resolution background pic for the close up shots.

    Reply
  3. John Nagle

    One problem I have with using this is that in my app, I want to be able to apply this to the group but also to items within the group, such that if you scale the group all items scale. Then, if you touch an item in the group, it should scale as well.

    In the current implementation, the scaled coordinates for items inside the group cause the items to translate incorrectly.

    -John

    Reply
  4. Matt Webster

    Hi John, you’re right and this is something I want to address in a follow up post. I can’t say when I’ll get to write this however, so I’ll see if I can help right here.
    Essentially, the problem is that the position of the objects (possibly including the tracking dots, if they’re added to a group) are all relative to the (0,0) of the display group, not the world coordinate, but the touch coordinates are relative to the screen (0,0).
    To get round this, you need to convert the world coordinates into group-relative coordinates. In the touch listener use object:contentToLocal( e.x,e.y ) – this will take the touch event x and y and convert it to an x and y value relative to the display group your tracking dots or pinch-zoomed display object are contained within.
    http://docs.coronalabs.com/api/type/DisplayObject/contentToLocal.html
    Good luck; Its certainly an interesting idea to have items which can be manipulated individually and also as a group. This is also one of the main problems with expanding the tutorial into a “How to Use” tutorial – its just too long to incorporate all the different scenarios and problems. I’ll do my best though!
    Matt.

    Reply
    • antonio

      Hi Matt,
      thank you very much for the tutorial, it’s great.
      just 1 question, in sample 10, how I can do to add a second rect and assign the same event??
      regards

      Reply
  5. Arivan Bastos

    Very nice tutorial and module.

    Just a question: how to use this module with scrollView widget? I want be able to drag the scrollContent (as usual) and use the multitouch pinch-zoom-rotation feature with each image inside the scroll container.

    I’m trying hard, but I can’t figure out how combine the “scrollWidget:takeFocus()” method and your approach.

    Reply
  6. Matthew Webster

    Hmm, good one. I’m not sure that you want to be associating the functionality between them…

    If you let the (any) scroll (view, widget, etc) take the focus, you will lose the ability to control the handling of the following multiple touches. This implicitly stops your multitouch effect from ever starting.

    Well, while writing this, I’ve taken a stab at modifying the final ‘sample11.lua’ file to add a simple, full-screen scrollView widget and add the multi-coloured object from the demo…

    If you open ‘sample11.lua’, go to line 214 and add the following code (note: I’ve left in the surrounding lines to show where to place the insert) you should see the multi-colour object in a light blue scrollView:

    – create display group to listen for new touches
    local group = display.newGroup()

    – insert this…
    local widget = require “widget”
    local scrollView = widget.newScrollView{
    width = display.contentWidth,
    height = display.contentHeight,
    scrollWidth = display.contentWidth,
    scrollHeight = display.contentHeight*2,
    }
    display.newRect(scrollView.content,0,0,display.contentWidth,display.contentHeight*2):setFillColor(150,150,255)
    – inserted code ends.

    scrollView:insert( group )

    – populate display group with objects
    local rect = display.newRect( group, 200, 200, 200, 100 )

    Now, I’ve just built this and tried it on my iPad and it works. There is one issue which I’ve mentioned in a previous post, on this page:

    As the scrollView moves (when you use one touch to scroll it) the relative position of the object being pinched effectively changes. Because of this, you would need to begin making use of the :contentToLocal() and :localToContent() functions. These would allow the touch points to take effect in the relative space of the scrolled display group (the scrollView, in this case) rather than the absolute world coordinates of the screen.

    I will be working on this and I hope to provide a follow-up tutorial on making use of pinch-zoom in a real world application, but for now I’ll leave this as an exercise for the reader.

    Please leave your comments and questions and I’ll do my best to help out.

    Reply
  7. Nathan

    Great tutorial Brent – this helped me a lot.

    One quick question – you talked about implementing “tap” inside the touch function, but then excluded to reduce complexity of the example. I have both but they are getting a little confused (sometimes both get triggered – resulting in the item moving a little when tapped).

    How can I get my touch function to either detect the simultaneous tap (and not do the touch work), or to handle the tap itself?

    I do need to catch the tap either way, as I don’t want it passed through to the item behind the one that is the current focus.

    Thanks,
    Nathan.

    Reply
  8. Matt

    I would recommend that you don’t attach a tap listener and that you simply handle the touch as a tap.

    The process is basically to check in the ended phases whether the touch has moved beyond a certain distance (“threshold”) and if it is released within a certain time (“window”).

    I recommend a threshold of about 10px and a window of 320 milliseconds.

    I did not handle this scenario in this tutorial because it is a bit beyond the requirements for a pinch-zoom handler. I do intend to address it when I write a more general multi-touch tutorial, but there are a lot of complex scenarios to cover.

    Reply
    • Nathan

      Thanks Matt – I’ll try that, but I’m assuming you meant “handle the tap as a touch”, not “handle the touch as a tap” as written?

      I’ll also have to make sure there is only one finger “tapping” too.

      (Sorry for calling you Brent too!)

      Reply
      • Matthew Webster

        I’ve been intending to write a more involved tap/touch/swipe tutorial for some time, but I need to improve my own knowledge before getting that far and this year simply hasn’t provided me with the time. Keep an eye out for it, but don’t hold your breath, as they say…

        Reply
  9. Kerem

    Matt, this is terrific! Thank you so very much for your time and effort.

    I am working on an festival app and need to display a map (png file) of festival grounds. The app uses StoryBoard scenes and the map is to go into a scene. Rotation is not so much of a necessity but zoom/pinch and move is. Your sample gets me real close to what I need to do.

    I am able to adapt the Sample 10 to show my map and move it / zoom etc but I am a little lost in transitioning this code to live within my StoryBoard scene and coexist with the other elements. There is a tab bar taking some space at the bottom so the image display area for the map should honor that too…

    Have ever applied this code to a StoryBoard? I would really appreciate it if you have another sample like that. Thank you very very much!

    Reply
    • Matt

      If I understand you correctly, you want to be able to use the sample as provided, but within a display group parent which may have moved from 0,0 on the screen. This is possible but would need the input coordinates to be adjusted. As mentioned above, I’ll be working on this challenge and did make some headway a few weeks ago. I will need to revisit it, but (as also mentioned) this year is not providing me with much personal time. Keep your eyes peeled!

      Reply
  10. Kerem

    Hi Matt,

    Thanks for your kind response. I was able to get Sample 10 adapted for my needs. Basically I have a StoryBoard driven app where one scene is a png based map image. I switch scenes using a tabbar at the bottom so the map snaps in right above the tabbar.

    I pretty much crammed all of Sample 10 into the create scene event and call a purge scene on didExit event. It is not the most ideal spread but it works!

    I do want to get to using the group method you outline in the sample 11 though. This will allow me to overlay other images such as arrows pointing out to something the user might choose in an earlier scene. I know I can do this using your multiple objects moving and zooming together approach in sample 11.

    My question, if you have a moment at some point, is how you would take your sample 11 and spread its contents into a StoryBoard Scene. I can most likely whack it all into create scene and it’ll probably work but if you were doing it how would you do it. No rush.

    Thanks much for all your help. This is fantastic.

    Reply
    • Matthew Webster

      To be honest, I’ve not really tried my code with storyboard, but I did put it into a scroll view, as you’ve seen.

      I don’t see why the storyboard approach would be problematic, as it doesn’t affect how the display objects are moved around.

      I think that you are battling with how to handle the touch listeners and various display objects used in the code sample though.

      When putting code into a storyboard function which you need to keep a reference to it is best to start thinking about your code structure up front. Do you want to have references outside the scene object, in the module, or do you want to create everything in the create function and run the whole scene as if it never leaves that function.

      One approach I’ve used in a recent (unfinished) game is to put almost everything into the create scene function and even attach the listeners within it. All the other functions are internal to the create function as well. This means that writing code within the other functions, such as enterScene, have access to the variables created in the createScene function. The one listener which can’t be attached within that function is the createScene listener, of course. And it is very important to clean out the scene when it exits.

      I would post the code here, but it’s long and this comment box won’t render it properly. If you like, I can post it in the forum, but be prepared to adjust your view on storyboard code somewhat.

      Reply
  11. Kerem

    Matt,

    Thank you very much for your continued input, support and willingness to share. I did exactly what you mention above. Placed almost everything in Sample 10 in createScene and it worked well. You have to remember createScene gets called once per run so it might be a problem but in my case I wanted to purge the scene since the map is so large so this is not a problem.

    I would love to see the code sample so if you could post it in the forum that would be terrific. You had a forum discussion spawned from this tutorial but it appears that it is on the old and now shutdown forum. It would be lovely to start a similar one in the new forum.

    Alternatively you can post it in response to my forum post titled : Image move, zoom, pinch & overlay question. I was looking for exactly what you teach here so you can imagine my delight in coming across your tutorial. Thank you once again.

    http://forums.coronalabs.com/topic/34330-image-move-zoom-pinch-overlay-question/

    Reply
  12. Alan

    Hi Matt, great tutorial, but I have one question.

    I have used your code to rotate/zoom etc, but I want to limit the amount that the user can zoom in/out.

    If I do something like this:

    if (#rect.dots > 1) then
    — calculate the average rotation of the tracking dots
    rotate = calcAverageRotation( rect.dots )

    – calculate the average scaling of the tracking dots
    scale = calcAverageScaling( rect.dots )

    – apply rotation to rect
    rect.rotation = rect.rotation + rotate

    – apply scaling to rect
    rect.xScale, rect.yScale = rect.xScale * scale, rect.yScale * scale

    –add some zoom restrictions
    if rect.xScale > 2 then
    rect.xScale, rect.yScale = 2, 2
    elseif rect.xScale < 0.3 then
    rect.xScale, rect.yScale = 0.3, 0.3
    end
    end

    My zooming is restricted the way I want, but if I keep on pinching/stretching, the object will continue to move as if it was zooming (although it won't be scaling).
    It's kind of a hard one to explain, if someone could copy my "–add some zoom restrictions" part in, they will see what I mean.

    Is there a nice easy way to fix this?

    Reply
  13. Matthew Webster

    In the section “Pinch Centre Translation” you’ll also need to modify the code which scales around the pinch centre. This is because it is impossible to place your fingers equal distances from the centre of the display object, so we also scale the distance between the centre of the tracking points and the display object. Apply the same limits to this scaling operation as you have to the scaling of the display object and you’ll be done.

    Reply
  14. poolem

    This was very helpful in implementing a solution that would allow the user to resize the display group due to device screen limitations.

    I used this reference:
    http://howto.oz-apps.com/2011/08/create-your-own-function-library-part-1.html
    to turn this into “multitouch.lua” library.

    I then added a ‘rotate’ property to the display group so that this feature can be disabled if desired.

    To implement this in main.lua:

    local touchGroup = require(“multitouch”) — this library
    touchGroup.rotate = false — new property to enable/diable rotation

    local screen = display.newImage( “assets/mycdss-bg.png”, 0, 0, display.contentWidth, display.contentHeight ) — something to place on the device screen that should be multitouchable

    touchGroup:insert( screen)

    Here is the modified lua file that became my library:

    module(…,package.seeall)

    – one more thing

    – turn on multitouch
    system.activate(“multitouch”)

    – which environment are we running on?
    local isDevice = (system.getInfo(“environment”) == “device”)

    – returns the distance between points a and b
    function lengthOf( a, b )
    local width, height = b.x-a.x, b.y-a.y
    return (width*width + height*height)^0.5
    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)
    local angle = radian*180/math.pi
    if angle 180) then
    a = a – 360
    elseif (a 1) then
    – calculate the average rotation of the tracking dots
    if (rect.rotate) then
    rotate = calcAverageRotation( rect.dots )
    end
    – calculate the average scaling of the tracking dots
    scale = calcAverageScaling( rect.dots )

    – apply rotation to rect
    rect.rotation = rect.rotation + rotate

    – apply scaling to rect
    rect.xScale, rect.yScale = rect.xScale * scale, rect.yScale * scale
    end

    – declare working point for the rect location
    local pt = {}

    – translation relative to centre point move
    pt.x = rect.x + (centre.x – rect.prevCentre.x)
    pt.y = rect.y + (centre.y – rect.prevCentre.y)

    – scale around the average centre of the pinch
    – (centre of the tracking dots, not the rect centre)
    pt.x = centre.x + ((pt.x – centre.x) * scale)
    pt.y = centre.y + ((pt.y – centre.y) * scale)

    – rotate the rect centre around the pinch centre
    – (same rotation as the rect is rotated!)
    pt = rotateAboutPoint( pt, centre, rotate, false )

    – apply pinch translation, scaling and rotation to the rect centre
    rect.x, rect.y = pt.x, pt.y

    – store the centre of all touch points
    rect.prevCentre = centre
    else — “ended” and “cancelled” phases
    print( e.phase, e.x, e.y )

    – remove the tracking dot from the list
    if (isDevice or e.numTaps == 2) then
    – get index of dot to be removed
    local index = table.indexOf( rect.dots, e.target )

    – remove dot from list
    table.remove( rect.dots, index )

    – remove tracking dot from the screen
    e.target:removeSelf()

    – store the new centre of all touch points
    rect.prevCentre = calcAvgCentre( rect.dots )

    – refresh tracking dot scale and rotation values
    updateTracking( rect.prevCentre, rect.dots )
    end
    end
    return true
    end

    – if the target is not responsible for this touch event return false
    return false
    end

    – attach pinch zoom touch listener
    touchGroup.touch = touch
    touchGroup.rotate = true
    – listen for touches starting on the touch object
    touchGroup:addEventListener(“touch”)

    return touchGroup
    _______________________

    -Thank you for providing this example!

    Reply
  15. Eric

    If I wanted to have the whole screen touchable rather than just the display objects, what would be the most effective way to do this?

    Reply
    • Matt

      There’s really only two ways to handle that. Either attach listeners to every display object or have one big object sitting in front if everything, with a listener. That object would also need to be invisible and has isHitTestable=true

      That’s really the solution whether you’re talking about regular touch listeners or the pinch zoom code above.

      Reply
      • Tomaz

        Hi

        I’m also very grateful for this tutorial. Just one question. As soon as I apply this code in my game, all other eventListeners stop wokring, they become unresponsive. Only your eventListener is working. How can I change this? Thank you for your help, Matt.

        Tomaz

        Reply
        • Matt

          Without seeing code, I can’t really tell. Have you created a forum thread for this?

          I would guess that you are either using single touch mode or inadvertently creating a touch layer on top of all the other layers, which is blocking touches from going through. If that is the case, each touch would potentially be creating a new tracking dot, rather than going to the target object.

          Matt.

          Reply

Leave a Reply

  • (Will Not Be Published)