Difference between revisions of "Game Camera"

From GiderosMobile
(added content)
 
(added Gcam)
(4 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
__TOC__
 
__TOC__
  
<languages />
+
Here you will find various resources to help you create games and apps in Gideros Studio.
  
<translate><!--T:1--> Here you will find various resources to help you create games and apps in Gideros Studio.</translate>
+
'''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'''
  
<translate><!--T:1--> '''note''':You may have to provide your own assets (fonts, gfx, …).</translate>
+
'''It has zoom, shake, follow, and more...'''
  
 +
'''Usage:'''
 +
<source 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>
  
=== <translate>Kinetic Zoom Camera</translate> ===
+
'''The full class'''
 +
<source 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 ===
 
<source lang="lua">
 
<source lang="lua">
 
-- Kinetic Zoom Camera
 
-- Kinetic Zoom Camera
Line 36: Line 750:
 
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 
-- THE SOFTWARE.
 
-- THE SOFTWARE.
 
  
 
--[[
 
--[[
 
+
This implements a camera class that allows the user to drag and zoom a virtual camera. It works
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
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
 
isn't really a camera that moves, rather it just moves itself and any child elements relative
to the devices normal view. Further, when the user lifts their finger in the middle of a drag, the
+
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.
 
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
+
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.
+
combine them, but could be modified to do so. It will change smoothly between the modes, however.
  
 
Usage:
 
Usage:
Line 78: Line 790:
  
 
function Camera:init(options)
 
function Camera:init(options)
options = options or {}
+
local options = options or {}
self.maxZoom = options.maxZoom or 2 -- Maximum scale allowed. 1 = normal unzoomed
+
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.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.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
 
self.minPoints = options.minPoints or 3 -- Minimum points to enable kinetic scroll
Line 137: Line 849:
 
end
 
end
  
-- Update our anchor point that's the middle of the "camera". This should be called
+
-- Update our anchor point. That's the middle of the "camera". This should be called
 
-- whenever you change the camera position
 
-- whenever you change the camera position
 
function Camera:updateAnchor()
 
function Camera:updateAnchor()
Line 144: Line 856:
 
end
 
end
  
-- Center our anchor. This should be called anytime you change the scale to recenter the
+
-- 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
 
-- 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
 
-- changed based on the 0,0 anchor of the sprite, but we want it to zoom based on the
Line 152: Line 864:
 
end
 
end
  
-- Center the camera on a point relitive to the child element(s)
+
-- Center the camera on a point relative to the child element(s)
 
function Camera:centerPoint(x, y)
 
function Camera:centerPoint(x, y)
 
self:setX(-(x * self:getScaleX() - application:getContentWidth()/2))
 
self:setX(-(x * self:getScaleX() - application:getContentWidth()/2))
Line 245: Line 957:
 
-- Clean up old points
 
-- Clean up old points
 
-- NOTE: This is not the most efficient way to implement a stack with tables
 
-- 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
+
-- in LUA, but it's the simplest and performs fine for our purposes
 
while #self.previousPoints > self.maxPoints do
 
while #self.previousPoints > self.maxPoints do
 
table.remove(self.previousPoints, 1)
 
table.remove(self.previousPoints, 1)
Line 358: Line 1,070:
 
end
 
end
 
</source>
 
</source>
<br/>
 

Revision as of 20:52, 9 December 2020

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:

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

The full class

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

Kinetic Zoom Camera

-- 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 = {{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 = {{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