Zooming Camera: Anchor point hell

I’m trying to write a simple 2D camera that can pan around a 2D scene and zoom in on specific points. I’ve been working on trying to get the camera to pan and zoom at the same point.

Panning is straightforward, but writing a generic zoom seems to be pretty tricky. I can get both to work if the scene group is positioned at 0,0. If the group is offset, the zoom breaks, and I’ve not been able to figure out how to handle that case. 

I’ve tried both setting anchorChildren=true (which breaks the zoom) and trying to derive the offset from within the camera.fixReference function.

Does anyone have any ideas of how to handle this case?

Here’s my test code:

main.lua

[lua]

require “camera”

local group=display.newGroup()

camera.view(group)

– changing these numbers will not influence the original scene, but will through the camera’s fixReference function completely off!

– group.anchorChildren=true uncomment to break

group.x,group.y=1000,0 – set to 0,0 to make everything work

local function createScene()

  local bg=display.newRect(group,display.contentCenterX,display.contentCenterY,display.contentWidth,display.contentHeight)

  bg.fill={0.5,0.2,0.2}

  bg:setStrokeColor(255, 0, 0)

  bg.strokeWidth=5

  local c=display.newCircle(group,display.contentCenterX,display.contentCenterY,20)

  c.fill={1,0,0}

  local ct=display.newCircle(display.contentCenterX,display.contentCenterY,5)

  ct:setFillColor(255,127)

  local cx,cy=group:contentToLocal( display.contentCenterX, display.contentCenterY)

  display.newLine(group,cx,cy,display.contentCenterX,display.contentCenterY).width=4

end

createScene()

function touch(event)

  if event.phase==“began” then

    touchId=event.id

  end

  if event.id~=touchId then 

    return false

  end

  if event.phase==“ended” then

    local x,y=group:contentToLocal(event.x,event.y)

     local dx,dy=10,10

    display.newLine(group,x,y,x+dx,y+dy).strokeWidth=4

    display.newLine(group,x,y,x-dx,y-dy).strokeWidth=4

    display.newLine(group,x,y,x-dx,y+dy).strokeWidth=4

    display.newLine(group,x,y,x+dx,y-dy).strokeWidth=4

   

    local t=camera.panTo(event.x,event.y)

    timer.performWithDelay(t,function()

      local t=camera.zoom(2,display.contentCenterX,display.contentCenterY)

      timer.performWithDelay( t, function() 

        camera.zoom(1,display.contentCenterX,display.contentCenterY)

      end)

    end)

  end

  return true

end

Runtime:addEventListener(“touch”,touch)

[/lua]

camera.lua

[lua]

local M={}

camera=M

local display=display

local transition=transition

local easing=easing

local print=print

local timer=timer

local debug=debug

local error=error

setfenv(1,M)

local group

local cameraTime=4000

function create()

  group=display.newGroup()

  group.anchorChildren=true

  group.anchorX,group.anchorY=0.5,0.5

  group.x,group.y=display.contentCenterX,display.contentCenterY

end

function view(obj)

  group:insert(obj)

end

function panTo(x,y)

  local dx,dy=display.contentCenterX-x,display.contentCenterY-y

  transition.cancel(“pan”)

  transition.cancel(“panzoom”)

  if dx*dx+dy*dy==0 then

    return 0

  end

  transition.to(group,{time=cameraTime,tag=“pan”,transition=easing.inOutCubic,x=group.x+dx,y=group.y+dy,onComplete=function()

  end})

  return cameraTime

end

local anchor

local function fixReference(x,y)

  if anchor then 

    anchor:removeSelf()

  end

   – old anchor coordinates

  local ox=(group.width*(group.anchorX-0.5))

  local oy=(group.height*(group.anchorY-0.5))

  local lx,ly=group:contentToLocal(x, y)

  – compute the new anchor

  group.anchorX=lx/group.width

  group.anchorY=ly/group.height

  anchor=display.newCircle(group,group.anchorX*group.width,group.anchorY*group.height,20)

  anchor.fill={0,1,0}

  – offset the group’s position to stop it jumping

  group.x=group.x-ox+(group.width*(group.anchorX-0.5))

  group.y=group.y-oy+(group.height*(group.anchorY-0.5))

end

function zoom(zoom,x,y)

  if group.xScale==zoom then

    return 0

  end

  if x and y and zoom>group.xScale then

    fixReference(x,y)

  end

  

  transition.to(group,{time=cameraTime,tag=“zoom”,transition=easing.inOutCubic,xScale=zoom,yScale=zoom})

  return cameraTime

end

create()

return M

[/lua]

I don’t know if you saw these links already, but here are some resources on implementing zoom into projects:

http://www.coronalabs.com/blog/2013/01/22/implementing-pinch-zoom-rotate/

http://developer.coronalabs.com/code/pinchzoom-made-real-easy

Thanks for the links, they look useful. I’d still be curious to know why the example above breaks (the code should just run in the simulator if you save them as main.lua and camera.lua respectively)

[Update]

I had a quick look through those examples, and I couldn’t see anything on zooming around an arbitrary point on the screen, which is I think the issue I’m having. Or rather I’m coming into problems when zooming in on an arbitrary point in an offset group.

same problem here. i have no working solution yet and i think there wont be any without some corona sdk changes/support

http://forums.coronalabs.com/topic/38529-preliminary-results-converting-real-world-app/#entry201661

ok. i’m back with our solution. previous camera system was essentialy very simple and elegant. it was just one line:
 
[lua]transition.to( worldGroup, { time=t_time, xReference=toX, yReference=toY, xScale=scale, yScale=scale, rotation=angleTo, x=deltaX, y=deltaY} )
[/lua]

  • xReference, yReference - position in the world where your camera is looking at (world coordinates)
  • scale, rotation - self explanatory. as scaling and rotation is happening around reference points, there are no issues. 
  • x,y - position on the screen where you want your world position to be placed/aligned (screen coordinates)

for example in our game “little galaxy”, camera (xReference,yReference) jumps from planet to planet, rotating to other planets…

x,y is mostly set at the bottom of the screen. there are exceptions, for example in main menu or if there is big jump etc. thats why we need to animate x,y

with graphics 2.0 this is not possible unless you turn on v1 compatibility mode (but is this long term solution?). anchors are not like references. anchors are not in world coordinates and they are restricted to 0,1 range. that means, we need to rotate and scale only around 0,0!!! only option is to split this group into 3 different nested groups and apply transitions step on them.

  • worldPositionGroup - change x,y to look with camera at some world position
  • scaleRotationGroup:insert(worldPositionGroup) - scale/zoom. as we are not translating this group but the child (worldPositionGroup), scaling/zooming works
  • screenPositionGroup:insert(scaleRotationGroup) - finally outmost group just positions/aligns everything on screen. if you want your focused object at screen center x,y = contentWidth*0.5,contentHeight*0.5

the final code is:

[lua]

transition.to( screenPositionGroup, { time=t_time, x=deltaX, y=deltaY } )
transition.to( scaleRotationGroup, { time=t_time, rotation=angleTo, xScale=scale, yScale=scale } )
transition.to( worldPositionGroup, { time=t_time, x=-toX, y=-toY } )   – notice the minus sign
[/lua]

as we have 2 more layers like this in the background with parallax effect and some trickery with “infinite texture bacground” the whole solution will look like mess probably, but i thing there is no better way until corona staff will do something with this.

i hope they will, but until then i hope this will help anyone with similar problem

scape

Thanks for that, I hadn’t thought of splitting up the problem like that. It actually gives a bit more control as you can use tags to just cancel part of the transition.

I’m not sure I really understood what you use the screenPositionGroup. Do you have any examples?

Tom

not at all.

btw. here is the full code. it generates some random circles and then focuses camera on the last one (white) and looks towards yellow one. 

play with screenX and screenY and you will see - in the expamle white circle will be 3/4 down the screen. if you put 0.5 there instead of 0.75, white cirlcle will end up in the middle of the screen.

also you can play with the first part of scale equation (display.contentHeight*0.5). it calculates camera zoom in a such way, that the screen distance between white and yellow circle will be half the display height.

scape

[lua]screenPositionGroup = display.newGroup()
scaleRotationGroup = display.newGroup()
worldPositionGroup = display.newGroup()
scaleRotationGroup:insert(worldPositionGroup)
screenPositionGroup:insert(scaleRotationGroup)

– some random circles
for i=1,10 do
    local obj = display.newCircle(math.random(400),math.random(400),math.random(15,25))
    obj:setFillColor(0,0,0.5,1)
    worldPositionGroup:insert(obj)
end

– white circle
local a = worldPositionGroup[worldPositionGroup.numChildren]
a:setFillColor(1,1,1,1)

– yellow circle
local b = worldPositionGroup[worldPositionGroup.numChildren-1]
b:setFillColor(1,1,0,1)

local t_time = 1000
local screenX = display.contentWidth*0.5
local screenY = display.contentHeight*0.75
local toX = a.x
local toY = a.y
local width, height = a.x-b.x, a.y-b.y
local scale = (display.contentHeight*0.5) / math.sqrt( width*width + height*height )

local angleTo = -math.ceil( math.atan2(height,width) * 180 / math.pi ) + 90    – +90 because axes are rotated

if (angleTo > 180) then – to not rotate excessively
    angleTo = angleTo - 360
end
transition.to( screenPositionGroup, { time=t_time, x=screenX, y=screenY } )
transition.to( scaleRotationGroup, { time=t_time, rotation=angleTo, xScale=scale, yScale=scale } )
transition.to( worldPositionGroup, { time=t_time, x=-toX, y=-toY } ) – minus sign - we need to bring our camera position back to 0,0[/lua]

Ah! Thanks for that, it all makes sense now! That’s a wonderful effect there, thanks for including it!

I’ve updated my camera module to use the three groups. The function names probably need to be renamed a little, and I’ve not put a rotate in as I don’t use it yet, but it’s going to be really useful.

Thanks again,

[lua]

local M={}

camera=M

local display=display

local transition=transition

local easing=easing

setfenv(1,M)

local worldPositionGroup – change x,y to look with camera at some world position

local scaleRotationGroup – scale/zoom. as we are not translating this group but the child (worldPositionGroup), scaling/zooming works

local screenPositionGroup – finally outmost group just positions/aligns everything on screen. if you want your focused object at screen center x,y = contentWidth*0.5,contentHeight*0.5

local cameraTime=4000

function create()

  worldPositionGroup=display.newGroup()

  scaleRotationGroup=display.newGroup()

  screenPositionGroup=display.newGroup()

  scaleRotationGroup:insert(worldPositionGroup)

  screenPositionGroup:insert(scaleRotationGroup)

  screenPositionGroup.x,screenPositionGroup.y=display.contentCenterX,display.contentCenterY

end

function view(obj)

  worldPositionGroup:insert(obj)

end

function panTo(x,y)

  x,y=worldPositionGroup:contentToLocal(x,y)

  transition.cancel(“pan”)

  transition.to(worldPositionGroup,{time=cameraTime,tag=“pan”,transition=easing.inOutCubic,x=-x,y=-y})

  return cameraTime

end

function zoom(zoom)

  transition.cancel(“zoom”)

  if scaleRotationGroup.xScale==zoom then

    return 0

  end

  transition.to(scaleRotationGroup,{time=cameraTime,tag=“zoom”,transition=easing.inOutCubic,xScale=zoom,yScale=zoom})

  return cameraTime

end

function centreCameraOn(x,y)

  transition.cancel(“focus”)

  

  transition.to(screenPositionGroup,{time=cameraTime,tag=“focus”,x=x,y=y,transition=easing.inOutCubic})

  return cameraTime

end

function stop()

  transition.cancel(worldPositionGroup)

  transition.cancel(scaleRotationGroup)

  transition.cancel(screenPositionGroup)

end

create()

return M

[/lua]

I don’t know if you saw these links already, but here are some resources on implementing zoom into projects:

http://www.coronalabs.com/blog/2013/01/22/implementing-pinch-zoom-rotate/

http://developer.coronalabs.com/code/pinchzoom-made-real-easy

Thanks for the links, they look useful. I’d still be curious to know why the example above breaks (the code should just run in the simulator if you save them as main.lua and camera.lua respectively)

[Update]

I had a quick look through those examples, and I couldn’t see anything on zooming around an arbitrary point on the screen, which is I think the issue I’m having. Or rather I’m coming into problems when zooming in on an arbitrary point in an offset group.

same problem here. i have no working solution yet and i think there wont be any without some corona sdk changes/support

http://forums.coronalabs.com/topic/38529-preliminary-results-converting-real-world-app/#entry201661

ok. i’m back with our solution. previous camera system was essentialy very simple and elegant. it was just one line:
 
[lua]transition.to( worldGroup, { time=t_time, xReference=toX, yReference=toY, xScale=scale, yScale=scale, rotation=angleTo, x=deltaX, y=deltaY} )
[/lua]

  • xReference, yReference - position in the world where your camera is looking at (world coordinates)
  • scale, rotation - self explanatory. as scaling and rotation is happening around reference points, there are no issues. 
  • x,y - position on the screen where you want your world position to be placed/aligned (screen coordinates)

for example in our game “little galaxy”, camera (xReference,yReference) jumps from planet to planet, rotating to other planets…

x,y is mostly set at the bottom of the screen. there are exceptions, for example in main menu or if there is big jump etc. thats why we need to animate x,y

with graphics 2.0 this is not possible unless you turn on v1 compatibility mode (but is this long term solution?). anchors are not like references. anchors are not in world coordinates and they are restricted to 0,1 range. that means, we need to rotate and scale only around 0,0!!! only option is to split this group into 3 different nested groups and apply transitions step on them.

  • worldPositionGroup - change x,y to look with camera at some world position
  • scaleRotationGroup:insert(worldPositionGroup) - scale/zoom. as we are not translating this group but the child (worldPositionGroup), scaling/zooming works
  • screenPositionGroup:insert(scaleRotationGroup) - finally outmost group just positions/aligns everything on screen. if you want your focused object at screen center x,y = contentWidth*0.5,contentHeight*0.5

the final code is:

[lua]

transition.to( screenPositionGroup, { time=t_time, x=deltaX, y=deltaY } )
transition.to( scaleRotationGroup, { time=t_time, rotation=angleTo, xScale=scale, yScale=scale } )
transition.to( worldPositionGroup, { time=t_time, x=-toX, y=-toY } )   – notice the minus sign
[/lua]

as we have 2 more layers like this in the background with parallax effect and some trickery with “infinite texture bacground” the whole solution will look like mess probably, but i thing there is no better way until corona staff will do something with this.

i hope they will, but until then i hope this will help anyone with similar problem

scape

Thanks for that, I hadn’t thought of splitting up the problem like that. It actually gives a bit more control as you can use tags to just cancel part of the transition.

I’m not sure I really understood what you use the screenPositionGroup. Do you have any examples?

Tom

not at all.

btw. here is the full code. it generates some random circles and then focuses camera on the last one (white) and looks towards yellow one. 

play with screenX and screenY and you will see - in the expamle white circle will be 3/4 down the screen. if you put 0.5 there instead of 0.75, white cirlcle will end up in the middle of the screen.

also you can play with the first part of scale equation (display.contentHeight*0.5). it calculates camera zoom in a such way, that the screen distance between white and yellow circle will be half the display height.

scape

[lua]screenPositionGroup = display.newGroup()
scaleRotationGroup = display.newGroup()
worldPositionGroup = display.newGroup()
scaleRotationGroup:insert(worldPositionGroup)
screenPositionGroup:insert(scaleRotationGroup)

– some random circles
for i=1,10 do
    local obj = display.newCircle(math.random(400),math.random(400),math.random(15,25))
    obj:setFillColor(0,0,0.5,1)
    worldPositionGroup:insert(obj)
end

– white circle
local a = worldPositionGroup[worldPositionGroup.numChildren]
a:setFillColor(1,1,1,1)

– yellow circle
local b = worldPositionGroup[worldPositionGroup.numChildren-1]
b:setFillColor(1,1,0,1)

local t_time = 1000
local screenX = display.contentWidth*0.5
local screenY = display.contentHeight*0.75
local toX = a.x
local toY = a.y
local width, height = a.x-b.x, a.y-b.y
local scale = (display.contentHeight*0.5) / math.sqrt( width*width + height*height )

local angleTo = -math.ceil( math.atan2(height,width) * 180 / math.pi ) + 90    – +90 because axes are rotated

if (angleTo > 180) then – to not rotate excessively
    angleTo = angleTo - 360
end
transition.to( screenPositionGroup, { time=t_time, x=screenX, y=screenY } )
transition.to( scaleRotationGroup, { time=t_time, rotation=angleTo, xScale=scale, yScale=scale } )
transition.to( worldPositionGroup, { time=t_time, x=-toX, y=-toY } ) – minus sign - we need to bring our camera position back to 0,0[/lua]

Ah! Thanks for that, it all makes sense now! That’s a wonderful effect there, thanks for including it!

I’ve updated my camera module to use the three groups. The function names probably need to be renamed a little, and I’ve not put a rotate in as I don’t use it yet, but it’s going to be really useful.

Thanks again,

[lua]

local M={}

camera=M

local display=display

local transition=transition

local easing=easing

setfenv(1,M)

local worldPositionGroup – change x,y to look with camera at some world position

local scaleRotationGroup – scale/zoom. as we are not translating this group but the child (worldPositionGroup), scaling/zooming works

local screenPositionGroup – finally outmost group just positions/aligns everything on screen. if you want your focused object at screen center x,y = contentWidth*0.5,contentHeight*0.5

local cameraTime=4000

function create()

  worldPositionGroup=display.newGroup()

  scaleRotationGroup=display.newGroup()

  screenPositionGroup=display.newGroup()

  scaleRotationGroup:insert(worldPositionGroup)

  screenPositionGroup:insert(scaleRotationGroup)

  screenPositionGroup.x,screenPositionGroup.y=display.contentCenterX,display.contentCenterY

end

function view(obj)

  worldPositionGroup:insert(obj)

end

function panTo(x,y)

  x,y=worldPositionGroup:contentToLocal(x,y)

  transition.cancel(“pan”)

  transition.to(worldPositionGroup,{time=cameraTime,tag=“pan”,transition=easing.inOutCubic,x=-x,y=-y})

  return cameraTime

end

function zoom(zoom)

  transition.cancel(“zoom”)

  if scaleRotationGroup.xScale==zoom then

    return 0

  end

  transition.to(scaleRotationGroup,{time=cameraTime,tag=“zoom”,transition=easing.inOutCubic,xScale=zoom,yScale=zoom})

  return cameraTime

end

function centreCameraOn(x,y)

  transition.cancel(“focus”)

  

  transition.to(screenPositionGroup,{time=cameraTime,tag=“focus”,x=x,y=y,transition=easing.inOutCubic})

  return cameraTime

end

function stop()

  transition.cancel(worldPositionGroup)

  transition.cancel(scaleRotationGroup)

  transition.cancel(screenPositionGroup)

end

create()

return M

[/lua]