Difference between revisions of "Game Camera"

From GiderosMobile
(added Gcam)
m (Text replacement - "<source" to "<syntaxhighlight")
Line 11: Line 11:
  
 
'''Usage:'''
 
'''Usage:'''
<source lang="lua">
+
<syntaxhighlight lang="lua">
 
-- yourScene is a Sprite in which you put all your graphics
 
-- yourScene is a Sprite in which you put all your graphics
 
camera = GCam.new(yourScene [, anchorX, anchorY]) -- anchor by default is (0.5, 0.5)
 
camera = GCam.new(yourScene [, anchorX, anchorY]) -- anchor by default is (0.5, 0.5)
Line 18: Line 18:
  
 
'''The full class'''
 
'''The full class'''
<source lang="lua">
+
<syntaxhighlight lang="lua">
 
local atan2,sqrt,cos,sin,log,random = math.atan2,math.sqrt,math.cos,math.sin,math.log,math.random
 
local atan2,sqrt,cos,sin,log,random = math.atan2,math.sqrt,math.cos,math.sin,math.log,math.random
 
local PI = math.pi
 
local PI = math.pi
Line 725: Line 725:
  
 
=== Kinetic Zoom Camera ===
 
=== Kinetic Zoom Camera ===
<source lang="lua">
+
<syntaxhighlight lang="lua">
 
-- Kinetic Zoom Camera
 
-- Kinetic Zoom Camera
 
-- https://github.com/nshafer/KineticZoomCamera
 
-- https://github.com/nshafer/KineticZoomCamera

Revision as of 14:28, 13 July 2023

Here you will find various resources to help you create games and apps in Gideros Studio.

note: you may have to provide your own assets (fonts, gfx, …).

GCam

A fantastic camera for Gideros from MultiPain https://github.com/MultiPain/Gideros_GCam

It has zoom, shake, follow, and more...

Usage: <syntaxhighlight lang="lua"> -- yourScene is a Sprite in which you put all your graphics camera = GCam.new(yourScene [, anchorX, anchorY]) -- anchor by default is (0.5, 0.5) stage:addChild(camera) </source>

The full class <syntaxhighlight lang="lua"> local atan2,sqrt,cos,sin,log,random = math.atan2,math.sqrt,math.cos,math.sin,math.log,math.random local PI = math.pi

-- ref: -- https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/a-brief-introduction-to-lerp-r4954/#:~:text=Linear%20interpolation%20(sometimes%20called%20'lerp,0..1%5D%20range. local function smoothOver(dt, smoothTime, convergenceFraction) return 1 - (1 - convergenceFraction)^(dt / smoothTime) end local function lerp(a,b,t) return a + (b-a) * t end local function clamp(v,mn,mx) return (v><mx)<>mn end local function map(v, minSrc, maxSrc, minDst, maxDst, clampValue) local newV = (v - minSrc) / (maxSrc - minSrc) * (maxDst - minDst) + minDst return not clampValue and newV or clamp(newV, minDst >< maxDst, minDst <> maxDst) end local function distance(x1,y1, x2,y2) return (x2-x1)^2 + (y2-y1)^2 end local function distanceSq(x1,y1, x2,y2) return sqrt((x2-x1)^2 + (y2-y1)^2) end local function angle(x1,y1, x2,y2) return atan2(y2-y1,x2-x1) end local function setMeshAsCircle(m, ox, oy, rad_in_x, rad_in_y, rad_out_x, rad_out_y, color, alpha, edges) edges = edges or 16 local step = (PI*2)/edges

local vi = m:getVertexArraySize() + 1 local ii = m:getIndexArraySize() + 1 local svi = vi local sii = ii

for i = 0, edges-1 do local ang = i * step local cosa = cos(ang) local sina = sin(ang)

local x_in = ox + rad_in_x * cosa local y_in = oy + rad_in_y * sina

local x_out = ox + rad_out_x * cosa local y_out = oy + rad_out_y * sina

m:setVertex(vi+0,x_in,y_in) m:setVertex(vi+1,x_out,y_out)

m:setColor(vi+0,color, alpha) m:setColor(vi+1,color, alpha)

vi += 2 if i <= edges-2 then local si = (svi-1)+((i+1)*2)-1 m:setIndex(ii+0,si) m:setIndex(ii+1,si+1) m:setIndex(ii+2,si+3) m:setIndex(ii+3,si) m:setIndex(ii+4,si+3) m:setIndex(ii+5,si+2) ii += 6 end end local si = (svi-1)+(edges*2)-1 m:setIndex(ii+0,si) m:setIndex(ii+1,si+1) m:setIndex(ii+2,svi)

m:setIndex(ii+3,si+1) m:setIndex(ii+4,svi+1) m:setIndex(ii+5,svi) end

local function outExponential(ratio) if ratio == 1 then return 1 end return 1-2^(-10 * ratio) end

GCam = Core.class(Sprite) GCam.SHAKE_DELAY = 10

function GCam:init(content, ax, ay) assert(content ~= stage, "bad argument #1 (сontent should be different from the 'stage')")

self.viewport = Viewport.new() self.viewport:setContent(content)

self.content = Sprite.new() self.content:addChild(self.viewport) self:addChild(self.content)

self.matrix = Matrix.new() self.viewport:setMatrix(self.matrix)

-- some vars self.w = 0 self.h = 0 self.ax = ax or 0.5 self.ay = ay or 0.5 self.x = 0 self.y = 0 self.zoomFactor = 1 self.rotation = 0

self.followOX = 0 self.followOY = 0

-- Bounds self.leftBound = -1000000 self.rightBound = 1000000 self.topBound = -1000000 self.bottomBound = 1000000

-- Shaker self.shakeTimer = Timer.new(GCam.SHAKE_DELAY, 1) self.shakeDistance = 0 self.shakeCount = 0 self.shakeAmount = 0 self.shakeTimer:addEventListener("timerComplete", self.shakeDone, self) self.shakeTimer:addEventListener("timer", self.shakeUpdate, self) self.shakeTimer:stop()

-- Follow -- 0 - instant move self.smoothX = 0.9 self.smoothY = 0.9 -- Dead zone self.deadWidth = 50 self.deadHeight = 50 self.deadRadius = 25 -- Soft zone self.softWidth = 150 self.softHeight = 150 self.softRadius = 75

--------------------------------------- ------------- debug stuff ------------- --------------------------------------- self.__debugSoftColor = 0xffff00 self.__debugAnchorColor = 0xff0000 self.__debugDotColor = 0x00ff00 self.__debugAlpha = 0.5

self.__debugRMesh = Mesh.new() self.__debugRMesh:setIndexArray(1,3,4, 1,2,4, 1,3,7, 3,5,7, 2,4,8, 4,8,6, 5,6,8, 5,8,7, 9,10,11, 9,11,12, 13,14,15, 13,15,16, 17,18,19, 17,19,20) self.__debugRMesh:setColorArray(self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha)

self.__debugCMesh = Mesh.new() --------------------------------------- --------------------------------------- --------------------------------------- self:setShape("rectangle") self:setAnchor(self.ax,self.ay) self:updateClip() end



DEBUG STUFF -------------------


function GCam:setDebug(flag) self.__debug__ = flag

if flag then if self.shapeType == "rectangle" then self.__debugCMesh:removeFromParent() self:addChild(self.__debugRMesh) elseif self.shapeType == "circle" then self.__debugRMesh:removeFromParent() self:addChild(self.__debugCMesh) end self:debugUpdate() self:debugUpdate(true, 0, 0) else self.__debugCMesh:removeFromParent() self.__debugRMesh:removeFromParent() end end

function GCam:switchDebug() self:setDebug(not self.__debug__) end

function GCam:debugMeshUpdate() local w,h = self.w, self.h local zoom = self.zoomFactor local rot = self.rotation

local ax,ay = w * self.ax,h * self.ay

local TS = 1 local off = w <> h

if self.shapeType == "rectangle" then local dw = (self.deadWidth * zoom) / 2 local dh = (self.deadHeight * zoom) / 2 local sw = (self.softWidth * zoom) / 2 local sh = (self.softHeight * zoom) / 2 --[[ Mesh vertices

1-----------------2 | \ soft zone / | | 3-----------4 | | | dead zone | | | 5-----------6 | | / \ | 7-----------------8 ]]

self.__debugRMesh:setVertexArray( ax-sw,ay-sh, ax+sw,ay-sh,

ax-dw,ay-dh, ax+dw,ay-dh, ax-dw,ay+dh, ax+dw,ay+dh,

ax-sw,ay+sh, ax+sw,ay+sh,

ax-TS,-off, ax+TS,-off, ax+TS,h+off, ax-TS,h+off,

-off,ay-TS, -off,ay+TS, w+off,ay+TS, w+off,ay-TS ) self.__debugRMesh:setAnchorPosition(ax,ay) self.__debugRMesh:setPosition(ax,ay) self.__debugRMesh:setRotation(rot) elseif self.shapeType == "circle" then --[[ Mesh:

-- first 4 vertex is green target point 1--2 | | 4--3

next, vertical anchor line 5--6 | | | | | | 8--7 next, horizontal anchor line 9--------10 | | 12-------11 and finaly, circle

8 edges "circle" look like this:

24--------------26--------------28 | \ soft zone | / | | 23-----------25-----------27 | | | | | | | dead | | 22--21 zone 13--14 | | | | | | | | | 19-----------17-----------15 | | / | \ | 20--------------18--------------16 ]] local dr = self.deadRadius * zoom local sr = self.softRadius * zoom

self.__debugCMesh:setVertexArray(0,0,0,0,0,0,0,0,ax-TS,-off, ax+TS,-off,ax+TS,h+off, ax-TS,h+off, -off,ay-TS, -off,ay+TS, w+off,ay+TS, w+off,ay-TS) self.__debugCMesh:setIndexArray(1,2,3, 1,3,4, 5,6,7, 5,7,8, 9,10,11, 9,11,12) self.__debugCMesh:setColorArray(self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha)

setMeshAsCircle(self.__debugCMesh, ax,ay, dr, dr, sr, sr, self.__debugSoftColor,self.__debugAlpha, 32)

self.__debugCMesh:setAnchorPosition(ax,ay) self.__debugCMesh:setPosition(ax,ay) self.__debugCMesh:setRotation(rot) end end

function GCam:debugUpdate(dotOnly, gx,gy) if self.__debug__ then if dotOnly then local zoom = self:getZoom() local ax = self.w * self.ax local ay = self.h * self.ay local size = 4 * zoom

local x = (gx * zoom - self.x * zoom) + ax local y = (gy * zoom - self.y * zoom) + ay if self.shapeType == "rectangle" then self.__debugRMesh:setVertex(17, x-size,y-size) self.__debugRMesh:setVertex(18, x+size,y-size) self.__debugRMesh:setVertex(19, x+size,y+size) self.__debugRMesh:setVertex(20, x-size,y+size) elseif self.shapeType == "circle" then self.__debugCMesh:setVertex(1, x-size,y-size) self.__debugCMesh:setVertex(2, x+size,y-size) self.__debugCMesh:setVertex(3, x+size,y+size) self.__debugCMesh:setVertex(4, x-size,y+size) end else self:debugMeshUpdate() end end end



RESIZE LISTENER -----------------


-- set camera size to window size function GCam:setAutoSize(flag) if flag then self:addEventListener(Event.APPLICATION_RESIZE, self.appResize, self) self:appResize() elseif self:hasEventListener(Event.APPLICATION_RESIZE) then self:removeEventListener(Event.APPLICATION_RESIZE, self.appResize, self) end end

function GCam:appResize() local minX,minY,maxX,maxY = application:getLogicalBounds() self.w = maxX+minX self.h = maxY+minY self.matrix:setPosition(self.w * self.ax,self.h * self.ay) self.viewport:setMatrix(self.matrix)

self:debugUpdate() self:updateClip() end



SHAPES ---------------------


function GCam:rectangle(dt,x,y) local sw = self.softWidth / 2 local sh = self.softHeight / 2 local dw = self.deadWidth / 2 local dh = self.deadHeight / 2

local dstX = self.x local dstY = self.y

-- X smoothing if x > self.x + dw then -- out of dead zone on right side local dx = x - self.x - dw local fx = smoothOver(dt, self.smoothX, 0.99) dstX = lerp(self.x, self.x + dx, fx) elseif x < self.x - dw then -- out of dead zone on left side local dx = self.x - dw - x local fx = smoothOver(dt, self.smoothX, 0.99) dstX = lerp(self.x, self.x - dx, fx) end -- clamp to soft zone dstX = clamp(dstX, x - sw,x + sw)

-- Y smoothing if y > self.y + dh then -- out of dead zone on bottom side local dy = y - self.y - dh local fy = smoothOver(dt, self.smoothY, 0.99) dstY = lerp(self.y, self.y + dy, fy) elseif y < self.y - dh then -- out of dead zone on top side local dy = self.y - dh - y local fy = smoothOver(dt, self.smoothY, 0.99) dstY = lerp(self.y, self.y - dy, fy) end -- clamp to soft zone dstY = clamp(dstY, y - sh,y + sh)

return dstX, dstY end

function GCam:circle(dt,x,y) local dr = self.deadRadius local sr = self.softRadius

local dstX, dstY = self.x, self.y

local d = distanceSq(self.x, self.y, x, y)

if d > dr and d <= sr then -- out of dead zone on bottom side local offset = d-dr local ang = angle(self.x, self.y, x, y) local fx = smoothOver(dt, self.smoothX, 0.99) local fy = smoothOver(dt, self.smoothY, 0.99) dstX = lerp(self.x, self.x + cos(ang) * offset, fx) dstY = lerp(self.y, self.y + sin(ang) * offset, fy) elseif d > sr then local ang = angle(self.x, self.y, x, y) local offset = d-sr+120*dt dstX = self.x + cos(ang) * offset dstY = self.y + sin(ang) * offset end

return dstX, dstY end

-- shapeType(string): function name -- can be "rectangle" or "circle" -- you can create custom shape by -- adding a new method to a class -- then use its name as shapeType function GCam:setShape(shapeType) self.shapeType = shapeType self.shapeFunction = self[shapeType] assert(self.shapeFunction ~= nil, "[GCam]: shape with name \""..shapeType.."\" does not exist") assert(type(self.shapeFunction) == "function", "[GCam]: incorrect shape type. Must be\"function\", but was: "..type(shapeFunction)) -- DEBUG -- self:setDebug(self.__debug__) self:debugUpdate() self:debugUpdate(true, 0, 0) end



UPDATE ---------------------


function GCam:update(dt) local obj = self.followObj if obj then local x,y = obj:getPosition()

x += self.followOX y += self.followOY

local dstX, dstY = self:shapeFunction(dt,x,y)

if self.x ~= dstX or self.y ~= dstY then self:goto(dstX,dstY) end

self:debugUpdate(true,x,y) end self:updateClip() end



FOLLOW ----------------------


function GCam:setFollow(obj) self.followObj = obj end

function GCam:setFollowOffset(x,y) self.followOX = x self.followOY = y end



SHAKE ----------------------


-- duration (number): time is s. -- distance (number): maximum shake offset function GCam:shake(duration, distance) self.shaking = true

self.shakeCount = 0 self.shakeDistance = distance or 100 self.shakeAmount = (duration*1000) // GCam.SHAKE_DELAY

self.shakeTimer:reset() self.shakeTimer:setRepeatCount(self.shakeAmount) self.shakeTimer:start() end

function GCam:shakeDone() self.shaking = false self.shakeCount = 0 self.content:setPosition(0,0) end

function GCam:shakeUpdate() self.shakeCount += 1 local amplitude = 1 - outExponential(self.shakeCount/self.shakeAmount) local hd = self.shakeDistance / 2 local x = random(-hd,hd)*amplitude local y = random(-hd,hd)*amplitude self.content:setPosition(x, y) end



ZONES ----------------------


-- Camera intepolate its position towards target -- w (number): soft zone width -- h (number): soft zone height function GCam:setSoftSize(w,h) self.softWidth = w self.softHeight = h or w self:debugUpdate() end

function GCam:setSoftWidth(w) self.softWidth = w self:debugUpdate() end

function GCam:setSoftHeight(h) self.softHeight = h self:debugUpdate() end -- r (number): soft zone radius (only if shape type is "circle") function GCam:setSoftRadius(r) self.softRadius = r self:debugUpdate() end

-- Camera does not move in dead zone -- w (number): dead zone width -- h (number): dead zone height function GCam:setDeadSize(w,h) self.deadWidth = w self.deadHeight = h or w self:debugUpdate() end

function GCam:setDeadWidth(w) self.deadWidth = w self:debugUpdate() end

function GCam:setDeadHeight(h) self.deadHeight = h self:debugUpdate() end

function GCam:setDeadRadius(r) self.deadRadius = r self:debugUpdate() end

-- Smooth factor -- x (number): -- y (number): function GCam:setSmooth(x,y) self.smoothX = x self.smoothY = y or x end

function GCam:setSmoothX(x) self.smoothX = x end

function GCam:setSmoothY(y) self.smoothY = y end



BOUNDS ---------------------


function GCam:updateBounds() local x = clamp(self.x, self.leftBound, self.rightBound) local y = clamp(self.y, self.topBound, self.bottomBound) if x ~= self.x or y ~= self.y then self:goto(x,y) end end

-- Camera can move only inside given bbox function GCam:setBounds(left, top, right, bottom) self.leftBound = left or 0 self.topBound = top or 0 self.rightBound = right or 0 self.bottomBound = bottom or 0

self:updateBounds() end

function GCam:setLeftBound(left) self.leftBound = left or 0 self:updateBounds() end

function GCam:setTopBound(top) self.topBound = top or 0 self:updateBounds() end

function GCam:setRightBound(right) self.rightBound = right or 0 self:updateBounds() end

function GCam:setBottomBound(bottom) self.bottomBound = bottom or 0 self:updateBounds() end

function GCam:getBounds() return self.leftBound, self.topBound, self.rightBound, self.bottomBound end



TRANSFORMATIONS -----------------


function GCam:move(dx, dy) self:goto(self.x + dx, self.y + dy) end

function GCam:zoom(value) local v = self.zoomFactor + value if v > 0 then self:setZoom(v) end end

function GCam:rotate(ang) self.rotation += ang self:setAngle(self.rotation) end



POSITION ----------------


function GCam:rawGoto(x,y) x = clamp(x, self.leftBound, self.rightBound) y = clamp(y, self.topBound, self.bottomBound) self.matrix:setAnchorPosition(x,y) self.viewport:setMatrix(self.matrix) end

function GCam:goto(x,y) x = clamp(x, self.leftBound, self.rightBound) y = clamp(y, self.topBound, self.bottomBound)

self.x = x self.y = y self.matrix:setAnchorPosition(x,y) self.viewport:setMatrix(self.matrix) end

function GCam:gotoX(x) x = clamp(x, self.leftBound, self.rightBound) self.x = x self.matrix:setAnchorPosition(x,self.y) self.viewport:setMatrix(self.matrix) end

function GCam:gotoY(y) y = clamp(y, self.topBound, self.bottomBound) self.y = y self.matrix:setAnchorPosition(self.x,y) self.viewport:setMatrix(self.matrix) end



ZOOM ------------------


function GCam:setZoom(zoom) self.zoomFactor = zoom self.matrix:setScale(zoom, zoom, 1) self.viewport:setMatrix(self.matrix) self:debugUpdate() end

function GCam:getZoom() return self.zoomFactor end



ROTATION ----------------


function GCam:setAngle(angle) self.rotation = angle self.matrix:setRotationZ(angle) self.viewport:setMatrix(self.matrix) self:debugUpdate() end

function GCam:getAngle() return self.matrix:getRotationZ() end



ANCHOR POINT --------------


function GCam:setAnchor(anchorX, anchorY) self.ax = anchorX self.ay = anchorY self.matrix:setPosition(self.w * anchorX,self.h * anchorY) self.viewport:setMatrix(self.matrix) self:debugUpdate() end

function GCam:setAnchorX(anchorX) self.ax = anchorX self.matrix:setPosition(self.w * anchorX,self.h * self.ay) self.viewport:setMatrix(self.matrix) self:debugUpdate() end

function GCam:setAnchorY(anchorY) self.ay = anchorY self.matrix:setPosition(self.w * self.ax,self.h * anchorY) self.viewport:setMatrix(self.matrix) self:debugUpdate() end

function GCam:getAnchor() return self.ax, self.ay end



SIZE ------------------


function GCam:updateClip() local ax = self.w * self.ax local ay = self.h * self.ay --self.viewport:setClip(self.x-ax,self.y-ay,self.w,self.h+ay) --self.viewport:setAnchorPosition(self.x,self.y) end

function GCam:setSize(w,h) self.w = w self.h = h

self:debugUpdate() self:updateClip() end </source>

Kinetic Zoom Camera

<syntaxhighlight lang="lua"> -- Kinetic Zoom Camera -- https://github.com/nshafer/KineticZoomCamera

-- The MIT License (MIT)

-- Copyright (c) 2013 Nathan Shafer

-- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions:

-- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software.

-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE.

--[[ This implements a camera class that allows the user to drag and zoom a virtual camera. It works basically by having child elements that are bigger than the view size of the given device. There isn't really a camera that moves, rather it just moves itself and any child elements relative to the devices normal view. Furthermore, when the user lifts their finger in the middle of a drag, the drag movement will continue with some kinetic energy and slow down based on simulated friction.

This has two distinct modes, DRAG and SCALE, based on how many touches are detected. It doesn't combine them, but could be modified to do so. It will change smoothly between the modes, however.

Usage:

local camera = Camera.new() local camera = Camera.new({maxZoom=1.5,friction=.5}) stage:addChild(camera)

-- Add whatever you want as a child of the camera local image = Bitmap.new(Texture.new("sky_world_big.png")) camera:addChild(image)

-- If you want to center the camera on a child element, such as a player, you can do: local player = Sprite.new() -- example player sprite camera:centerPoint(player:getX(), player:getY())

-- If you want to process touch events relative to where the camera is, you can translate the event function onTouchBegin(event) local point = camera:translateEvent(event) -- point.x = x position of the touch relative to the camera -- point.y = y position of the touch relative to the camera end --]]

Camera = Core.class(Sprite)

-- Constants Camera.DRAG = 1 Camera.SCALE = 2

function Camera:init(options) local options = options or {} self.maxZoom = options.maxZoom or 2 -- Maximum scale allowed. 1 = normal unzoomed self.friction = options.friction or .85 -- Percentage to slow the drag down by on every frame. Lower = more slippery self.maxPoints = options.maxPoints or 10 -- Number of history points to keep in memory self.minPoints = options.minPoints or 3 -- Minimum points to enable kinetic scroll

-- These are tables that store a history of touch events and times self.previousPoints = nil self.previousTimes = nil

-- We maintain an anchor that is the center of the "camera" self.anchorX = 0 self.anchorY = 0

-- Add our event listeners for touch events self:addEventListener(Event.TOUCHES_BEGIN, self.onTouchesBegin, self) self:addEventListener(Event.TOUCHES_MOVE, self.onTouchesMove, self) self:addEventListener(Event.TOUCHES_END, self.onTouchesEnd, self) self:addEventListener(Event.TOUCHES_CANCEL, self.onTouchesCancel, self) end

-- Override the Sprite position functions so that we can enforce boundaries function Camera:setX(x) -- override -- Check boundaries x = math.max(x, application:getContentWidth() - self:getWidth()) x = math.min(x, 0)

Sprite.setX(self, x) end

function Camera:setY(y) -- override -- Check boundaries y = math.max(y, application:getContentHeight() - self:getHeight()) y = math.min(y, 0)

Sprite.setY(self, y) end

function Camera:setPosition(x, y) -- override self:setX(x) self:setY(y or x) end

-- Override the Sprite setScale function so we can enforce boundaries function Camera:setScale(scaleX, scaleY) -- override -- Calculate boundaries local minScaleX = application:getContentWidth() / (self:getWidth() * (1/self:getScaleX())) local minScaleY = application:getContentHeight() / (self:getHeight() * (1/self:getScaleY()))

-- Check the boundaries scaleX = math.max(scaleX, minScaleX, minScaleY) scaleX = math.min(scaleX, self.maxZoom)

scaleY = math.max(scaleY or scaleX, minScaleY, minScaleX) scaleY = math.min(scaleY or scaleX, self.maxZoom)

Sprite.setScale(self, scaleX, scaleY) end

-- Update our anchor point. That's the middle of the "camera". This should be called -- whenever you change the camera position function Camera:updateAnchor() self.anchorX = (-self:getX() + application:getContentWidth()/2) * (1/self:getScaleX()) self.anchorY = (-self:getY() + application:getContentHeight()/2) * (1/self:getScaleY()) end

-- Center our anchor. This should be called anytime you change the scale to recenter the -- view on the anchor, which will get moved by changing the scale since scaling will be -- changed based on the 0,0 anchor of the sprite, but we want it to zoom based on the -- center of the camera view function Camera:centerAnchor() self:centerPoint(self.anchorX, self.anchorY) end

-- Center the camera on a point relative to the child element(s) function Camera:centerPoint(x, y) self:setX(-(x * self:getScaleX() - application:getContentWidth()/2)) self:setY(-(y * self:getScaleY() - application:getContentHeight()/2))

self:updateAnchor() end

-- Translate the x/y coordinates of an event to the cameras coordinates. It -- takes both position and scale into consideration. function Camera:translateEvent(event) local point = {x=0,y=0}

point.x = (-self:getX() + event.x or event.touch.x) * (1/self:getScaleX()) point.y = (-self:getY() + event.y or event.touch.y) * (1/self:getScaleY())

return(point) end

-- Calculate distance between two points function Camera:getDistance(p1, p2) local dx = p2.x - p1.x local dy = p2.y - p1.y

return(math.sqrt(dx^2 + dy^2)) end

-- Stop the camera from moving any more function Camera:stop() self:removeEventListener(Event.ENTER_FRAME, self.onEnterFrame, self) self.velocity = nil self.time = nil end

-- A finger or mouse is pressed function Camera:onTouchesBegin(event) if self:hitTestPoint(event.touch.x, event.touch.y) then self.isFocus = true

if #event.allTouches <= 1 then self.mode = Camera.DRAG

-- Record the starting point self.x0 = event.touch.x self.y0 = event.touch.y

-- Stop any current camera movement self:stop()

-- Initialize our touch histories self.previousPoints = Template:X=event.touch.x,y=event.touch.y self.previousTimes = {os.timer()} else self.mode = Camera.SCALE

-- Only look at the last finger to touch, ignore intermediate fingers if event.touch.id == event.allTouches[#event.allTouches].id then -- Figure out initial distance self.initialDistance = self:getDistance(event.touch, event.allTouches[1]) self.initialScale = self:getScale() self.initialX = self:getX() self.initialY = self:getY() end end

event:stopPropagation() end end

function Camera:onTouchesMove(event) if self.isFocus then if self.mode == Camera.DRAG then -- Figure out how far we moved since last time local dx = event.touch.x - self.x0 local dy = event.touch.y - self.y0

-- Move the camera self:setX(self:getX() + dx) self:setY(self:getY() + dy)

-- Update our location self.x0 = event.touch.x self.y0 = event.touch.y

-- Update the anchor point self:updateAnchor()

-- Add to the stack for velocity calculations later table.insert(self.previousPoints, {x=event.touch.x,y=event.touch.y}) table.insert(self.previousTimes, os.timer())

-- Clean up old points -- NOTE: This is not the most efficient way to implement a stack with tables -- in LUA, but it's the simplest and performs fine for our purposes while #self.previousPoints > self.maxPoints do table.remove(self.previousPoints, 1) table.remove(self.previousTimes, 1) end elseif self.mode == Camera.SCALE then if #event.allTouches > 1 then -- Only look at the last finger to touch, ignore intermediate fingers if event.touch.id == event.allTouches[#event.allTouches].id then -- Figure out current distance local currentDistance = self:getDistance(event.touch, event.allTouches[1])

-- Change our scale self:setScale(currentDistance / self.initialDistance * self.initialScale)

-- Center on our anchor self:centerAnchor() end end end

event:stopPropagation() end end

function Camera:onTouchesEnd(event) if self.isFocus then if self.mode == Camera.DRAG then if self.previousPoints and #self.previousPoints > self.minPoints then -- calculate vectors between now and x points ago local new_time = os.timer() local vx = event.touch.x - self.previousPoints[1].x local vy = event.touch.y - self.previousPoints[1].y local vt = new_time - self.previousTimes[1]

-- Calculate our velocities self.velocity = {x=vx/vt, y=vy/vt} self.time = new_time

-- add an event listener to finish drawing the movement self:addEventListener(Event.ENTER_FRAME, self.onEnterFrame, self) end

self.isFocus = false elseif self.mode == Camera.SCALE then -- If we're left with just 2 touches, then go back to DRAG mode if #event.allTouches == 2 then self.mode = Camera.DRAG

-- reset our last position based on whatever finger is left for a smooth -- transition back to DRAG mode if event.allTouches[1].id == event.touch.id then self.x0 = event.allTouches[2].x self.y0 = event.allTouches[2].y else self.x0 = event.allTouches[1].x self.y0 = event.allTouches[1].y end

-- Reset our histories self:stop()

self.previousPoints = Template:X=self.x0,y=self.y0 self.previousTimes = {os.timer()} end end

event:stopPropagation() end end

function Camera:onTouchesCancel(event) if self.isFocus then print("Camera TOUCHES_CANCEL", self.mode) self.isFocus = false event:stopPropogation() end end

-- This will continue moving the camera based on the velocities that were imparted on it, -- eventually slowing to a stop based on the friction. function Camera:onEnterFrame(event) if self.mode == Camera.DRAG then -- Figure out how much time has passed since the last frame local new_time = os.timer() local dt = new_time - self.time self.time = new_time

-- Calculate the distance we should move this frame local sx = self.velocity.x * dt local sy = self.velocity.y * dt

-- Apply friction self.velocity.x = self.velocity.x * self.friction self.velocity.y = self.velocity.y * self.friction

-- Check if we're slow enough to just stop if math.abs(self.velocity.x) < .1 then self.velocity.x = 0 end if math.abs(self.velocity.y) < .1 then self.velocity.y = 0 end

if self.velocity.x == 0 and self.velocity.y == 0 then self:stop() else -- Move us self:setX(self:getX() + sx) self:setY(self:getY() + sy)

-- Update our anchor self:updateAnchor() end end end </source>