For some time I have been wanting to write a usable pinch zoom with rotate algorithm. Finally, I think I have a serviceable piece.
So, first off, usage:
Give the code an image (I'm using yoda.jpg)
Run the code
Tap once to create the first or two touch points
Tap somewhere else to create the second of the two touch points
Drag one of the two touch points around to scale, rotate and move the image
Tap once anywhere to remove the touch points and begin again
The ultimate effect:
What happens is that the two blue/red circles represent a pair of points used to pinch/zoom an image, just like in Apple's "Photo" app. The touch points remain where they are on the image when moved. The means that the image must be moved and scaled appropriately. Therefore, the rotation is based on the angle of the first point from the second. The scaling is based on the change of distance between the two points. The translation of the image happens around the point precisely halfway between the two touch points. That is also where all of the rotation and scaling of the image is applied.
Simulator:
I have only implemented single touch tracking, so if you want proper multitouch you will have to implement this yourself, but that is a pretty good exercise for any Corona developer. Obviously, if you are doing this in the simulator you need to double tap the red/blue touch points to move them and you can only move one at a time.
Blobs:
The blue circles with red outlines represent the touch points of, say, fingers on the device.
The white circle shows the mid-point between the touch points and scales with the change of distance between the touch points.
The green dot is the centre of the image and moves relative to the white mid-point, with scaling and rotation affected.
What is really happening:
A display group is added to the global display group stack. The white circle and green dot are added to this. The touch points are added to the global stack. In your code these would be handled elsewhere and probably not rendered. Using the mathlib library, the display group is moved, rotated and scaling and the image is translated to keep centred on the display group's centre. This is the real trick: The display group is moved, rotated and scaled and those are then mapped onto the img. At no point is the image transposed into the display group - it remains where you put it in the display hierarchy at all times. Finally, the positions in the system are calculated using localToContent and contentToLocal, so the img and display group can be added to their own parent display groups and the result should be the same. The reason here is that two fingers on the screen basically only ever work in content coordinates.
Anyway, have fun - here is all the code (provide your own image)...
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 | local mathlibapi = require("mathlib") display.setStatusBar( display.HiddenStatusBar ) local panel = nil local stage = display.getCurrentStage() local img = display.newImage( "yoda.png" ) -- you need to provide your own image here!!! img.x, img.y = display.contentCenterX, display.contentCenterY -- x_local, y_local = object:contentToLocal(x_content, y_content) -- x_content, y_content = object:localToContent(x, y) -- create a display object which will be directly rotated and scaled around it's own centre function beginPinch(img, grip) panel = display.newGroup() panel.img = img panel.grip = grip panel.initAngle = mathlibapi.angleBetweenPoints( panel.grip[1], panel.grip[2] ) panel.initImgAngle = img.rotation panel.initImgXScale, panel.initImgYScale = img.xScale, img.yScale panel.initScale = panel.xScale panel.x, panel.y = grip[1].x + (grip[2].x-grip[1].x) / 2, grip[1].y + (grip[2].y-grip[1].y) / 2 panel.initLength = mathlibapi.lengthOf( grip[1], grip[2] ) print('initlen',panel.initLength) -- get x,y of img in world local x, y = img:localToContent(0,0) -- get x,y of img in panel x, y = panel:contentToLocal(x,y) panel.centre = display.newCircle( panel, 0, 0, 50 ) panel.target = display.newCircle( panel, x, y, 10 ) panel.target:setFillColor( 0,255,0 ) panel.alpha = .7 end -- manipulate the display object and apply the changes to the target img function doPinch() -- rotation local a = mathlibapi.angleBetweenPoints( panel.grip[1], panel.grip[2] ) print( 'angle', a - panel.initAngle ) panel.rotation = a - panel.initAngle panel.img.rotation = panel.initImgAngle + (a - panel.initAngle) panel.x, panel.y = (panel.grip[1].x + panel.grip[2].x) / 2, (panel.grip[1].y + panel.grip[2].y) / 2 -- scaling local len = mathlibapi.lengthOf( panel.grip[1], panel.grip[2] ) panel.xScale = panel.initScale * (len / panel.initLength) panel.yScale = panel.initScale * (len / panel.initLength) panel.img.xScale = panel.initImgXScale * (len / panel.initLength) panel.img.yScale = panel.initImgYScale * (len / panel.initLength) print('scale',panel.xScale,panel.yScale) -- location -- convert target to world, then to panel.img.parent coords local x, y = panel.target:localToContent(0,0) x, y = panel.img.parent:contentToLocal(x, y) -- set img x,y panel.img.x, panel.img.y = x, y return true end -- remove the manipulation object function endPinch() panel:removeSelf() panel = nil stage[2]:removeSelf() stage[2]:removeSelf() Runtime:removeEventListener( "tap", endPinch ) timer.performWithDelay( 1, function() Runtime:addEventListener("tap",tap); end ) end function pinch( event ) if (event.phase == "began") then stage:setFocus( event.target ) elseif (event.phase == "moved") then event.target.x, event.target.y = event.x, event.y doPinch() else stage:setFocus( nil ) end return true end function tap(event) local circle = display.newCircle( event.x, event.y, 25 ) circle:setStrokeColor( 255,0,0 ) circle:setFillColor( 0,0,255 ) circle.strokeWidth = 4 circle.alpha = .7 print(stage.numChildren) if (stage.numChildren == 3) then Runtime:removeEventListener( "tap", tap ) stage[2]:addEventListener( "touch", pinch ) stage[3]:addEventListener( "touch", pinch ) timer.performWithDelay( 1, function() Runtime:addEventListener( "tap", endPinch ); end ) beginPinch(img, { stage[2], stage[3] } ) end return true end Runtime:addEventListener("tap",tap) |
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 | module(..., package.seeall) function lengthOf( a, b ) local width, height = b.x-a.x, b.y-a.y return math.sqrt(width*width + height*height) end function convertDegreesToRadians( degrees ) return (math.pi * degrees) / 180 end 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 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 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 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 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 -- 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 --[[ 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))) ]]-- |