Skip to content

Instantly share code, notes, and snippets.

@JMV38
Created October 27, 2012 05:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JMV38/3963106 to your computer and use it in GitHub Desktop.
Save JMV38/3963106 to your computer and use it in GitHub Desktop.
Real time controllers and menus
--# Main
-- Moving a sprite with a VirtualStick
displayMode(FULLSCREEN)
function setup()
pos = vec2(WIDTH/2, HEIGHT/2)
steer = vec2(0,0)
speed = 400 -- pixels per second
local func = function(v)
if v == "red" then tint(255, 0, 0, 255) end
if v == "green" then tint(0, 255, 31, 255) end
if v == "blue" then tint(0, 49, 255, 255) end
if v == "normal" then tint(255, 255, 255, 255) end
end
info = InfoBloc({side="right", x0=0.6 , x1=0.8 } )
local textfunc = function(v)
if v == "short" then info:changeTxt("coucou") end
if v == "empty" then info:changeTxt() end
if v == "long" then info:changeTxt("this text has to be really really long because "
.."want to have a carriage retirn that occurs") end
if v == "popup enable" then info.popupEnabled = true end
if v == "popup disable" then info.popupEnabled = false end
end
local list
menu1 = Menu({side="bottom", x0=0.6, x1=0.74,
list={"nothing",true,"reset",true}, callback=resetTest})
menu2 = Menu({side="top", pos=0.8, width=0.15,
list={"empty",true,"short",true,"long",true,
"popup enable",true,"popup disable",true,
}, callback=textfunc})
menu3 = Menu({side="left", pos=0.5, width=0.15,
list={"red",true,"green",false,"blue",true,"normal",true}, callback=func})
menu4 = Menu({side="right", pos=0.9, width=0.15,
list={"red",false,"green",false,"blue",true,"normal",true}, callback=func})
controller2 = VirtualStick {
moved = function(v) steer = v end,
released = function(v) steer = vec2(0,0) end,
x0=0.5,x1=1,y0=0.25,y1=0.49,name="moving stick",nameSide="right"}
controller4 = VirtualSlider{
moved = function(v) steer = vec2(0,v) end,
released = function(v) steer = vec2(0,0) end,
orientation = vec2(0,1), timeout=2,
x0=0.2,x1=0.4,y0=0.5,y1=1,name="slider",nameSide="top"}
allControllers = All({controller1,controller2,controller3,controller4,
menu1, menu2, menu3, menu4, info})
fps = FPS()
end
function resetTest(v)
if v=="reset" then
allControllers:resetDemo()
end
end
function draw()
background(0, 0, 0, 255)
pos = pos + steer*speed*DeltaTime
sprite("Planet Cute:Character Boy", pos.x, pos.y)
allControllers:draw()
fps:draw()
end
function touched(touch)
allControllers:touched(touch)
end
--# InfoBloc
InfoBloc = class()
function InfoBloc:init(args)
-- layout
self.title = args.title or "info"
self.txt = args.txt or "the side bar is highlighted when info is present"
self.side = args.side or "bottom"
self.textPos = args.textPos or vec2(WIDTH/2,HEIGHT-100)
self.textWidth = args.textWidth or 700
self.popupEnabled = args.popupEnabled or true
self.t0 = ElapsedTime
self.timeout = 0.5 -- seconds before txt vanishes
-- settings by pos and w
local x = args.pos
local w = args.width or 0.09
-- or by x0 and x1
if args.x0 and args.x1 then
x = (args.x0 + args.x1)/2
w = args.x1 - args.x0
end
local w1,w2,h1,h2
local pos
local dx0,dy0,dx,dy
local htxt = 30
local w0,h0 = math.floor( w * WIDTH ), math.floor( w * HEIGHT )
if self.side == "top" then
w1,w2,h1,h2 = w0,w0,htxt,htxt
pos = vec2(x*WIDTH, HEIGHT-h1/2)
dx0,dy0,dx,dy = 0, -h1-5, 0, -h2-2
elseif self.side == "bottom" then
w1,w2,h1,h2 = w0,w0,htxt,htxt
pos = vec2(x*WIDTH, h1/2)
dx0,dy0,dx,dy = 0,h1+5, 0, h2+2
elseif self.side == "left" then
w1,w2,h1,h2 = htxt,w0,h0,htxt
pos = vec2(w1/2, x*HEIGHT)
dx0,dy0,dx,dy = w1/2+5+w2/2, h1/2-h2/2, 0, -h2-2
elseif self.side == "right" then
w1,w2,h1,h2 = htxt,w0,h0,htxt
pos = vec2(WIDTH-w1/2, x*HEIGHT)
dx0,dy0,dx,dy = -w1/2-5-w2/2, h1/2-h2/2, 0, -h2-2
end
self.img = self:calcImg(self.title,w1,h1,self.side)
-- position of images
local x,y = pos.x,pos.y
self.pos = {}
self.radius = {}
self.pos["title"] = pos
self.radius["title"] = vec2(w1/2,h1/2)
-- manage various drawing states
self.deployed = false
self.highlighted = true -- the title is highlighted
end
function InfoBloc:resetDemo()
-- do nothing
end
function InfoBloc:changeTxt(txt)
if txt then
self.txt = txt
self.highlighted = true -- the title is highlighted
else
self.txt = nil
self.highlighted = false -- the title is not highlighted
end
-- reset timing
if self.popupEnabled then
self.t0 = ElapsedTime
self.finishing = true
end
end
function InfoBloc:calcImg(txt,w,h,rot)
local w0,h0
pushStyle() pushMatrix()
if rot=="left" or rot=="right"
then w0,h0 = h,w
else w0,h0 = w,h
end
local img0 = image(w0,h0)
setContext(img0)
font("AmericanTypewriter-Bold")
rectMode(CENTER)
textMode(CENTER)
strokeWidth(1)
background(255, 255, 255, 184)
fill(0, 0, 0, 255)
stroke(0, 0, 0, 255)
fontSize(20)
text(txt,w0/2,h0/2)
setContext()
local img = image(w,h)
setContext(img)
background(255, 255, 255, 184)
spriteMode(CENTER)
translate(w/2,h/2)
if rot=="left" then rotate(-90) end
if rot=="right" then rotate(90) end
sprite(img0,0,0)
setContext()
popStyle() popMatrix()
return img
end
function InfoBloc:draw()
pushStyle()
spriteMode(CENTER)
-- menu title
if self.highlighted then
local n = 1
tint(255,255,255,128 + (math.cos(ElapsedTime*2*math.pi*n)+1)/2*128 )
else tint(255,255,255,128)
end
local pos = self.pos
-- menu button
sprite(self.img, pos["title"].x, pos["title"].y)
-- deployed or finishing
if self.deployed or self.finishing then
local isel = self.selected
local delta = (self.timeout-(ElapsedTime - self.t0))/self.timeout
if delta<0 then finishing = false end
local alpha = 255
if self.finishing and not self.deployed then alpha = 255*delta end
font("AmericanTypewriter-Bold")
textMode(CENTER)
textAlign(LEFT)
textWrapWidth(self.textWidth)
strokeWidth(1)
fill(194, 194, 194, alpha)
stroke(0, 0, 0, 255)
fontSize(20)
if self.txt then text(self.txt,self.textPos.x,self.textPos.y) end
end
popStyle()
end
function InfoBloc:titleTouched(touch)
local pos,radius= self.pos["title"],self.radius["title"]
local goodZone = false
if math.abs((touch.x-pos.x))<radius.x
and math.abs((touch.y-pos.y))<radius.y
then
goodZone = true
end
self.choiceDone = false
return goodZone
end
function InfoBloc:touched(t)
if self:titleTouched(t) and t.state == BEGAN then
self.deployed=true
self.highlighted = false -- stop flickering
end
if t.state == ENDED and self.deployed then
self.deployed=false
-- reset timing
self.t0 = ElapsedTime
self.finishing = true
end
end
--# Menu
Menu = class()
function Menu:init(args)
-- actions
self.callback = args.callback or doNothing
-- layout
self.title = args.title or "menu"
local list = args.list or {"choice1",true,"choice2",true,"choice3",true}
self.list,self.disabled = {},{}
local imax = #list/2
for i=1,imax do
self.list[i] = list[2*i-1]
self.disabled[i] = not list[2*i]
end
self.selected = args.selected or 1
self.side = args.side or "bottom"
-- settings by pos and w
local x = args.pos
local w = args.width or 0.09
-- or by x0 and x1
if args.x0 and args.x1 then
x = (args.x0 + args.x1)/2
w = args.x1 - args.x0
end
local w1,w2,h1,h2
local pos
local dx0,dy0,dx,dy
local htxt = 30
local w0,h0 = math.floor( w * WIDTH ), math.floor( w * HEIGHT )
if self.side == "top" then
w1,w2,h1,h2 = w0,w0,htxt,htxt
pos = vec2(x*WIDTH, HEIGHT-h1/2)
dx0,dy0,dx,dy = 0, -h1-5, 0, -h2-2
elseif self.side == "bottom" then
w1,w2,h1,h2 = w0,w0,htxt,htxt
pos = vec2(x*WIDTH, h1/2)
dx0,dy0,dx,dy = 0,h1+5, 0, h2+2
elseif self.side == "left" then
w1,w2,h1,h2 = htxt,w0,h0,htxt
pos = vec2(w1/2, x*HEIGHT)
dx0,dy0,dx,dy = w1/2+5+w2/2, h1/2-h2/2, 0, -h2-2
elseif self.side == "right" then
w1,w2,h1,h2 = htxt,w0,h0,htxt
pos = vec2(WIDTH-w1/2, x*HEIGHT)
dx0,dy0,dx,dy = -w1/2-5-w2/2, h1/2-h2/2, 0, -h2-2
end
-- create text images
self.img = {}
for i,txt in ipairs(self.list) do
self.img[i] = self:calcImg(txt,w2,h2)
end
self.img["title"] = self:calcImg(self.title,w1,h1,self.side)
-- position of images
local x,y = pos.x,pos.y
self.pos = {}
self.radius = {}
self.pos["title"] = pos
self.radius["title"] = vec2(w1/2,h1/2)
for i,img in ipairs(self.img) do
if i==1 then x,y = x+dx0,y+dy0
else x,y = x+dx,y+dy end
self.pos[i] = vec2(x,y)
self.radius[i] = vec2(w2/2,h2/2)
end
-- manage various drawing states
self.deployed = false
self.highlighted = nil -- the choice highlighted
end
function Menu:resetDemo()
-- do nothing
end
function Menu:calcImg(txt,w,h,rot)
local w0,h0
pushStyle() pushMatrix()
if rot=="left" or rot=="right"
then w0,h0 = h,w
else w0,h0 = w,h
end
local img0 = image(w0,h0)
setContext(img0)
font("AmericanTypewriter-Bold")
rectMode(CENTER)
textMode(CENTER)
strokeWidth(1)
background(255, 255, 255, 184)
fill(0, 0, 0, 255)
stroke(0, 0, 0, 255)
fontSize(20)
text(txt,w0/2,h0/2)
setContext()
local img = image(w,h)
setContext(img)
background(255, 255, 255, 184)
spriteMode(CENTER)
translate(w/2,h/2)
if rot=="left" then rotate(-90) end
if rot=="right" then rotate(90) end
sprite(img0,0,0)
setContext()
popStyle() popMatrix()
return img
end
function Menu:draw()
pushStyle()
spriteMode(CENTER)
-- menu title
if self.highlighted then
else tint(255,255,255,128)
end
local pos = self.pos
-- menu button
sprite(self.img["title"], pos["title"].x, pos["title"].y)
-- deployed
if self.deployed then
local isel = self.selected
for i,img in ipairs(self.img) do
pushStyle()
if self.disabled[i] then tint(127, 127, 127, 129)
elseif i==isel then tint(255,255,255,255)
else tint(255,255,255,128) end
sprite(img, pos[i].x, pos[i].y)
popStyle()
end
end
popStyle()
end
function Menu:titleTouched(touch)
local pos,radius= self.pos["title"],self.radius["title"]
local goodZone = false
if math.abs((touch.x-pos.x))<radius.x
and math.abs((touch.y-pos.y))<radius.y
then
goodZone = true
end
self.choiceDone = false
return goodZone
end
function Menu:selectTouched(touch)
local pos,radius
for i,v in ipairs(self.pos) do
pos,radius = self.pos[i],self.radius[i]
if math.abs((touch.x-pos.x))<radius.x
and math.abs((touch.y-pos.y))<radius.y
and not self.disabled[i]
then self.selected = i self.choiceDone=true
end
end
end
function Menu:touched(t)
if self:titleTouched(t) and t.state == BEGAN then
self.deployed=true
self.initialSelect = self.selected
end
if self.deployed then
self:selectTouched(t)
end
if t.state == ENDED and self.deployed then
self.deployed=false
if self.choiceDone then
self.callback(self.list[self.selected])
end
end
end
--# Controller
-- Base class for controllers
--
-- Controllers translate touch events into callbacks to functions
-- that do something in the app. (Model/View/Controller style).
--
-- Controllers can draw a representation of their current state on
-- the screen, but you can choose not to.
--
-- A controller can be installed as the global handler for touch
-- events by calling its activate() method
Controller = class()
function Controller:activate(input)
self.x0 = input.x0 or 0
self.x1 = input.x1 or 1
self.y0 = input.y0 or 0
self.y1 = input.y1 or 1
self.cx = (self.x0+self.x1)/2 *WIDTH
self.cy = (self.y0+self.y1)/2 *HEIGHT
self.wx = (self.x1- self.x0)/2 *WIDTH
self.wy = (self.y1- self.y0)/2 *HEIGHT
self.name = input.name
self.nameSide = input.nameSide
if self.nameSide then
self:textPanel()
end
self.timeout = input.timeout or 2
self.t0 = ElapsedTime
end
function Controller:resetDemo()
self.t0 = ElapsedTime
end
function Controller:textPanel()
local w,h = 20,20
local side = self.nameSide
if side=="top" or side=="bottom" then w = self.wx*2
elseif side=="left" or side=="right" then w = self.wy*2
else print("the text for nameSide is not recognized")
end
local img = image(w,h)
setContext(img)
pushStyle() pushMatrix()
strokeWidth(1)
rectMode(RADIUS)
background(127, 127, 127, 57)
fill(0, 0, 0, 255)
fontSize(20)
text(self.name,w/2,h/2)
popStyle() popMatrix()
setContext()
local imgActive = image(w,h)
setContext(imgActive)
pushStyle() pushMatrix()
strokeWidth(1)
rectMode(RADIUS)
background(255, 255, 255, 184)
fill(0, 0, 0, 255)
fontSize(20)
text(self.name,w/2,h/2)
popStyle() popMatrix()
setContext()
local side = self.nameSide
local x,y,r
if side=="top" then
x,y,r = self.cx , self.y1*HEIGHT - img.height/2 , 0
elseif side=="bottom" then
x,y,r = self.cx , self.y0*HEIGHT + img.height/2 , 0
elseif side=="left" then
x,y,r = self.x0*WIDTH + img.height/2 , self.cy , -90
elseif side=="right" then
x,y,r = self.x1*WIDTH - img.height/2 , self.cy , 90
end
self.tx, self.ty, self.tr = x,y,r
self.imgPassive = img
self.imgActive = imgActive
end
function Controller:demo(timeout)
if (ElapsedTime - self.t0)<timeout then
pushStyle() pushMatrix()
strokeWidth(1)
rectMode(RADIUS)
fill(255, 255, 255, 46)
rect(self.cx,self.cy,self.wx,self.wy)
fill(255, 255, 255, 255)
fontSize(20)
text("touch here to",self.cx,self.cy+20)
text(self.name,self.cx,self.cy - 20)
popStyle() popMatrix()
end
end
function Controller:check(touch)
local goodZone = false
if math.abs((touch.x-self.cx))<self.wx
and math.abs((touch.y-self.cy))<self.wy
then
goodZone = true
end
return goodZone
end
function Controller:drawName()
local img
if self.touchId
then img = self.imgActive
else img = self.imgPassive
end
local x,y,r = self.tx, self.ty, self.tr
pushMatrix()
translate(x,y)
rotate(r)
spriteMode(CENTER)
sprite(img ,0,0)
popMatrix()
end
-- Utility functions
function touchPos(t)
return vec2(t.x, t.y)
end
function clamp(x, min, max)
return math.max(min, math.min(max, x))
end
function clampAbs(x, maxAbs)
return clamp(x, -maxAbs, maxAbs)
end
function clampLen(vec, maxLen)
if vec == vec2(0,0) then
return vec
else
return vec:normalize() * math.min(vec:len(), maxLen)
end
end
-- projects v onto the direction represented by the given unit vector
function project(v, unit)
return v:dot(unit)
end
function sign(x)
if x == 0 then
return 0
elseif x < 0 then
return -1
elseif x > 0 then
return 1
else
return x -- x is NaN
end
end
function doNothing()
end
--# Controller_VirtualStick
-- A virtual analogue joystick with a dead-zone at the center,
-- which activates wherever the user touches their finger
--
-- Arguments:
-- radius - radius of the stick (default = 100)
-- deadZoneRadius - radius of the stick's dead zone (default = 25)
-- moved(v) - Called when the stick is moved
-- v : vec2 - in the range vec2(-1,-1) and vec2(1,1)
-- pressed() - Called when the user starts using the stick (optional)
-- released() - Called when the user releases the stick (optional)
VirtualStick = class(Controller)
function VirtualStick:init(args)
self.radius = args.radius or 100
self.deadZoneRadius = args.deadZoneRadius or 25
self.releasedCallback = args.released or doNothing
self.steerCallback = args.moved or doNothing
self.pressedCallback = args.pressed or doNothing
self.activated = args.activate or true
-- pre-draw sprites
self.base = self:createBase()
self.stick = self:createStick()
if self.activated then self:activate(args) end
end
function VirtualStick:createBase()
local base = image(self.radius*2+6,self.radius*2+6)
pushStyle() pushMatrix()
ellipseMode(RADIUS)
strokeWidth(1)
stroke(255, 255, 255, 255)
noFill()
setContext(base)
background(0, 0, 0, 0)
ellipse(base.width/2, base.height/2, self.radius, self.radius)
ellipse(base.width/2, base.height/2, self.deadZoneRadius, self.deadZoneRadius)
setContext()
popMatrix() popStyle()
return base
end
function VirtualStick:createStick()
local base = image(56,56)
pushStyle() pushMatrix()
ellipseMode(RADIUS)
strokeWidth(1)
stroke(255, 255, 255, 255)
noFill()
setContext(base)
background(0, 0, 0, 0)
ellipse(base.width/2, base.height/2, 25, 25)
setContext()
popMatrix() popStyle()
return base
end
function VirtualStick:touched(t)
local pos = touchPos(t)
local goodZone = self:check(t)
if t.state == BEGAN and self.touchId == nil and goodZone then
self.touchId = t.id
self.touchStart = pos
self.stickOffset = vec2(0, 0)
self.pressedCallback()
elseif t.id == self.touchId then
if t.state == MOVING then
self.stickOffset = clampLen(pos - self.touchStart, self.radius)
self.steerCallback(self:vector())
elseif t.state == ENDED or t.state == CANCELLED then
self:reset()
self.releasedCallback()
end
end
end
function VirtualStick:vector()
local stickRange = self.radius - self.deadZoneRadius
local stickAmount = math.max(self.stickOffset:len() - self.deadZoneRadius, 0)
local stickDirection = self.stickOffset
if stickDirection:len()>0 then stickDirection = self.stickOffset:normalize() end
return stickDirection * (stickAmount/stickRange)
end
function VirtualStick:reset()
self.touchId = nil
self.touchStart = nil
self.stickOffset = nil
end
function VirtualStick:draw()
if self.nameSide then self:drawName() end
if self.name then self:demo(self.timeout) end
if self.touchId then
sprite(self.base,self.touchStart.x, self.touchStart.y)
sprite(self.stick,
self.touchStart.x+self.stickOffset.x,
self.touchStart.y+self.stickOffset.y)
end
end
--# Controller_VirtualSlider
-- A virtual analogue slider with a dead-zone at the center,
-- which activates wherever the user touches their finger
--
-- Arguments:
-- orientation - A unit vector that defines the orientation of the slider.
-- For example orientation=vec2(1,0) creates a horizontal slider,
-- orientation=vec2(0,1) creates a vertical slider. The slider
-- can be given an arbitrary orientation; it does not have to be
-- aligned with the x or y axis. For example, setting
-- orientation=vec2(1,1):normalize() creates a diagonal slider.
-- radius - Distance from the center to the end of the slider (default = 100)
-- deadZoneRadius - Distance from the center to the end of the dead zone (default = 25)
-- moved(x) - Called when the slider is moved
-- x : float - in the range -1 to 1
-- pressed() - Called when the user starts using the slider (optional)
-- released() - Called when the user releases the slider (optional)
VirtualSlider = class(Controller)
function VirtualSlider:init(args)
self.orientation = args.orientation or vec2(1,0)
self.radius = args.radius or 100
self.deadZoneRadius = args.deadZoneRadius or 25
self.releasedCallback = args.released or doNothing
self.movedCallback = args.moved or doNothing
self.pressedCallback = args.pressed or doNothing
self.activated = args.activate or true
self.base = self:createBase()
self.stick = self:createStick()
if self.activated then self:activate(args) end
end
function VirtualSlider:touched(t)
local pos = touchPos(t)
local goodZone = self:check(t)
if t.state == BEGAN and self.touchId == nil and goodZone then
self.touchId = t.id
self.touchStart = pos
self.sliderOffset = 0
self.pressedCallback()
elseif t.id == self.touchId then
if t.state == MOVING then
local v = pos - self.touchStart
self.sliderOffset = clampAbs(project(v, self.orientation), self.radius)
self.movedCallback(self:value())
elseif t.state == ENDED or t.state == CANCELLED then
self:reset()
self.releasedCallback()
end
end
end
function VirtualSlider:reset()
self.touchId = nil
self.touchStart = nil
self.sliderOffset = nil
end
function VirtualSlider:value()
local range = self.radius - self.deadZoneRadius
local amount = sign(self.sliderOffset) * math.max(math.abs(self.sliderOffset) - self.deadZoneRadius, 0)
return amount/range
end
function VirtualSlider:createBase()
local img = image(self.radius*2+6,self.radius*2+6)
setContext(img)
pushStyle()
ellipseMode(RADIUS)
strokeWidth(3)
stroke(255, 255, 255, 255)
lineCapMode(SQUARE)
noFill()
background(0, 0, 0, 0)
local function polarLine(orientation, fromRadius, toRadius)
local from = orientation * fromRadius + vec2(1,1)*(self.radius + 3)
local to = orientation * toRadius + vec2(1,1)*(self.radius + 3)
line(from.x, from.y, to.x, to.y)
end
polarLine(self.orientation, self.deadZoneRadius, self.radius)
polarLine(self.orientation, -self.deadZoneRadius, -self.radius)
popStyle()
setContext()
return img
end
function VirtualSlider:createStick()
local img = image(56,56)
setContext(img)
pushStyle() pushMatrix()
ellipseMode(RADIUS)
strokeWidth(3)
stroke(255, 255, 255, 255)
lineCapMode(SQUARE)
noFill()
background(0, 0, 0, 0)
strokeWidth(1)
ellipse(28, 28, 25, 25)
popMatrix() popStyle()
setContext()
return img
end
function VirtualSlider:draw()
if self.nameSide then self:drawName() end
if self.name then self:demo(self.timeout) end
if self.touchId then
pushMatrix() pushStyle()
spriteMode(CENTER)
sprite(self.base,self.touchStart.x, self.touchStart.y)
local sliderPos = self.orientation * self.sliderOffset + self.touchStart
strokeWidth(1)
sprite(self.stick, sliderPos.x, sliderPos.y)
popStyle() popMatrix()
end
end
--# Controller_All
All = class(Controller)
-- Forwards each touch event to all the controllers in the table
-- passed to the constructor
function All:init(controllers)
self.controllers = controllers
end
function All:touched(t)
for _, c in pairs(self.controllers) do
c:touched(t)
end
end
function All:resetDemo()
for _, c in pairs(self.controllers) do
c:resetDemo()
end
end
function All:draw()
for _, c in pairs(self.controllers) do
c:draw()
end
end
--# FPS
FPS = class()
function FPS:init()
self.val = 60
end
function FPS:draw()
-- update FPS value with some smoothing
self.val = self.val*0.99+ 1/(DeltaTime)*0.01
-- write the FPS on the screen
fill(208, 208, 208, 255)
fontSize(30)
font("AmericanTypewriter-Bold")
rectMode(CENTER)
text(math.floor(self.val).." fps",50,HEIGHT-25)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment