Skip to content

Instantly share code, notes, and snippets.

@apendley
Last active December 16, 2015 07:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save apendley/5401761 to your computer and use it in GitHub Desktop.
Save apendley/5401761 to your computer and use it in GitHub Desktop.
tparameter, a libary for Codea that allows parameters to be bound to arbitrary tables (as opposed to only _G, which is all Codea's parameter function allows). Project contains the tparameter class, as well as the Timer class, which is used by the example code. See Main and Ellipse for many examples of how to use tparameter. See http://twolivesle…
--# tparameter
-- tparameter
-- v1.0.2
local _updateRate = 1/60
local _refreshDelay = 1/60
local _reloadSteps, _reloadStepInterval = 4, 1
local _chooseParam = 'choose_object_to_inspect'
local _selectParam = 'SELECT'
local _chooseWatchParam = 'object_parameter_inspector'
local tparameter, _current, _selected, _visible, _inspect
local _refresher, _updater, _reloader
local _inspectorMessage, _inspectorInfo
local _min, _max, _floor, _mod = math.min, math.max, math.floor, math.mod
local tostring, tonumber = tostring, tonumber
local pairs, ipairs = pairs, ipairs
local tinsert = table.insert
local unpack, select = unpack, select
local parameter = parameter
-- unique identifier class
local ID = class()
function ID:init(idx)
idx = tostring(idx)
self[idx] = true
self.__default = idx
end
function ID:add(name, idx)
local _names = self.__names
if not names then
_names = {}
_names[name] = true
self.__names = _names
end
self[name .. idx] = true
end
function ID:get(id)
if id then return self[id] and id end
return self.__default
end
function ID:has(name)
local _names = self.__names
return _names and _names[name]
end
local function _isOwn(self, k, d)
return (k == d) or (k == '__names') or (k == '__default') or false
end
function ID:__tostring()
local default = self.__default
local s = '(' .. default
for k, v in pairs(self) do
if not _isOwn(self, k, default) then
s = s .. ', ' .. k
end
end
s = s .. ')'
return s
end
-- function to create memoized tables
local function memoize(mode, fn)
local memos = {}
if not fn then fn, mode = mode, fn end
if mode then
if mode == 'k' then
setmetatable(memos, weakKeyMT)
elseif mode == 'v' then
setmetatable(memos, weakValueMT)
elseif mode == 'kv' or mode == 'vk' then
setmetatable(memos, weakKeyValueMT)
end
end
local function _memoize(key)
if key == nil then return end
local memo = memos[key]
if not memo then
memo = fn(key, memos)
memos[key] = memo
end
return memo
end
return _memoize, memos
end
-- timer class
local Timer = class()
function Timer:init(...)
self.__eventdata = {}
self:start(...)
end
function Timer:start(interval, iterations, cb, ...)
assert(interval and iterations and cb)
self.active, self.accum, self.times = true, 0, 0
self.interval, self.cb = interval, cb
self.iterations = _max(iterations, 0)
if select('#', ...) == 0 then
if self.params and self.params.reuse then
self.params.reuse = nil
else
self.params = nil
end
else
self.params = {...}
end
return self
end
function Timer:restart(...)
local interval, iterations, cb =
self.interval, self.iterations, self.cb
if select('#', ...) == 0 and self.params then
self.params.reuse = true
self:start(interval, iterations, cb)
else
self:start(self.interval, self.iterations, self.cb, ...)
end
end
function Timer:stop()
if not self.active then return end
self.active, self.accum, self.times = false, 0, 0
end
function Timer:invokeCallback(e, params)
if params then
self.cb(unpack(params))
else
self.cb()
end
end
function Timer:update(dt)
local accum, interval = self.accum, self.interval
accum = accum + dt
if accum >= interval then
local stop = false
local e, iterations = self.__eventdata, self.iterations
while (not stop) and accum >= interval do
if iterations > 0 and self.times == iterations - 1 then
e.dt = accum
e.last = true
else
if accum < interval * 2 then
e.dt = accum
else
e.dt = interval + _mod(accum, interval)
end
end
accum = accum - interval
self.accum = accum
self.times = self.times + 1
e.n = self.times
if iterations > 0 and self.times == iterations then
self:stop(); stop = true
end
local params = self.params
e.timer = self
self:invokeCallback(e, params)
e.timer, e.dt, e.n, e.last = nil
end
else
self.accum = accum
end
return self.active
end
local TimerE = class(Timer)
function TimerE:invokeCallback(e, params)
if params then
self.cb(e, unpack(params))
else
self.cb(e)
end
end
-- refresh the inspector after a short delay
local function _refresh(...)
if _updater and _updater.active then _updater:stop() end
if not _refresher then
_refresher = Timer(_refreshDelay, 1, ...)
else
_refresher:start(_refreshDelay, 1, ...)
end
end
-- update the inspector message
local function _setInspectorMessage()
if _inspectorMessage then
_G[_chooseWatchParam] = _inspectorInfo .. '\n' .. _inspectorMessage
else
_G[_chooseWatchParam] = _inspectorInfo
end
end
-- reload inspector when inspector state changes
local function _delayedReload()
if not _reloader then
_reloader = TimerE(_reloadStepInterval, _reloadSteps, function(e)
if e.last then
_inspectorMessage = nil
tparameter.reload()
elseif e.n < math.max(_reloadSteps, 3) then
_inspectorMessage = '(Reloading in '
.. (_reloadSteps - e.n)
.. '...or press SELECT to reload now)'
else
_inspectorMessage = nil
end
_setInspectorMessage()
end)
else
_reloader:restart()
end
end
-- returns true if string is a metavariable
local function _ismetavariable(s)
return s:find('%[') or s:find('%.')
end
-- converts a metavariable token into an index
local function _mvindex(path)
local s, first = path, path:sub(1, 1)
if first == '.' or first == '[' then
s = path:sub(2, -1)
else
first = '.'
end
local b, d = s:find('%['), s:find('%.')
local next = (b and d) and math.min(b, d) or (b or d)
if first == '.' then
if next then
return s:sub(1, next-1), s:sub(next, -1)
else
return s:sub(1, -1)
end
elseif first == '[' then
local cbracket = s:find('%]')
if not cbracket or (next and next < cbracket) then
error('malformed metavariable path')
elseif next then
local index = s:sub(1, cbracket-1)
return tonumber(index) or index, s:sub(next, -1)
else
local index = s:sub(1, -2)
return tonumber(index) or index
end
end
end
-- returns the leaf object and index of a metavariable
local function _metavariable(object, path)
if not object then
return nil, path
end
local index, remainder = _mvindex(path)
if not remainder then
return object, index
end
return _metavariable(object[index], remainder)
end
-- for some reason Codea converts all non-alphanumeric
-- characters to '_' when creating the global variable
-- name for a parameter, so we have to do the same.
local function _sanitizeGlobal(indexPath)
local s, e, exp, v, fmt = indexPath:find('%['), indexPath:find('%]')
while(s and e) do
exp = indexPath:sub(s, e)
v = exp:sub(2, -2)
fmt = '.' .. (tonumber(v) and 'x%sx' or '%s')
exp = '%[' .. v .. '%]'
indexPath = indexPath:gsub(exp, fmt:format(v))
s, e = indexPath:find('%['), indexPath:find('%]')
end
indexPath = indexPath:gsub('%W', '_')
return indexPath
end
-- global variable name for parameter
local function _global(obj, name, variable)
if obj == _G then return _sanitizeGlobal(variable) end
return '_' .. name .. '_' .. _sanitizeGlobal(variable)
end
-- memoized counters for parameter object names
local _names = memoize(function() return {count = 0} end)
-- memoized parameter lists for objects
local _curObjID, __plistIndex = 0, {}
local _plist, __plist = memoize(function()
_curObjID = _curObjID + 1
local id = ID(_curObjID)
local plist = {__id = id}
__plistIndex[tonumber(id:get())] = plist
return plist
end)
-- return the number of parameter lists
local function _plistCount()
local c = 0
for k, v in pairs(__plist) do c = c + 1 end
return c
end
-- returns true if any parameter lists exist
local function _plistIsEmpty()
for k, v in pairs(__plist) do return false end
return true
end
-- return parameter list at specified pseudo index
local function _plistIndex(index)
local c = 0
for i = 1, table.maxn(__plistIndex) do
local v = __plistIndex[i]
if v then
c = c + 1
if c == index then return v end
end
end
end
-- get the pseudo index of a parameter list
local function _plistIndexOf(plistID)
local c = 0
for i = 1, table.maxn(__plistIndex) do
local v = __plistIndex[i]
if v then
c = c + 1
if v.__id == plistID then return c end
end
end
end
-- find an object via one of it's names
local function _findObject(objname)
for k, v in pairs(__plist) do
if v.__id:get(objname) == objname then return k end
end
end
-- return an object using a generic index.
-- index may be a number, parameter list name, or table
local function _index(index)
if type(index) == 'table' or type(index) == 'userdata' then
return index
elseif type(index) == 'number' then
index = tostring(index)
end
if type(index) == 'string' then
return _findObject(index)
end
end
-- create a new parameter for an object
local function _newParam(global, name, obj, variable, init, getter)
local pdata = {
global = global,
variable = variable,
init = init,
get = getter
}
local plist = _plist(obj)
-- don't add a new pdata if one
-- already exists for this variable
local found = false
for i, v in ipairs(plist) do
if v.variable == variable then
found, v.init, v.get = true, init, getter
break
end
end
if not found then tinsert(plist, pdata) end
local id = plist.__id
if not id:has(name) then
local counter = _names(name)
counter.count = counter.count + 1
id:add(name, counter.count)
end
if _visible then
_inspectorMessage = nil
_setInspectorMessage()
_delayedReload()
end
return pdata
end
-- set an object as selected in the inspector; sends 'highlight' notification
local function _setSelected(object)
if _selected == object then return end
if _selected then
local plist = __plist[_selected]
if plist and plist._selected then
plist._selected('highlight', false)
end
end
_selected = object
if _selected then
local plist = __plist[_selected]
if plist and plist._selected then
plist._selected('highlight', true)
end
end
end
-- set the current object; sends 'select' notifications
local function _setCurrent(object)
if _current == object then return end
if _current then
local plist = __plist[_current]
if plist and plist._selected then
plist._selected('select', false)
end
end
_current = object
if _current then
local plist = __plist[_current]
if plist and plist._selected then
plist._selected('select', true)
end
end
end
-- remove globals used by current object's parameters
local function _removeGlobals()
if not _current then return end
local plist = __plist[_current]
for _, p in ipairs(plist) do
if _current ~= _G or _ismetavariable(p.variable) then
if _G[p.global] then _G[p.global] = nil end
end
end
end
-- clear the current inspector parameters
local function _clear(keepCurrent)
_setSelected()
_visible = false
parameter.clear()
if _current and not keepCurrent then
_removeGlobals()
_setCurrent()
end
if _updater then _updater:stop() end
if _reloader then _reloader:stop() end
if _refresher then _refresher:stop() end
end
-- suspends reload temporarily when a parameter changes
local function _parameterChanged()
if _reloader and _reloader.active then
_inspectorMessage = nil
_setInspectorMessage()
_delayedReload()
end
end
-- make a function to set the object field
local function _makeSetter(obj, variable, filter, cb)
local setter
if _ismetavariable(variable) then
setter = function(value)
value = filter and filter(value) or value
local o, v = _metavariable(obj, variable)
if o and v then o[v] = value end
_parameterChanged()
return (o and v) and value or (filter and filter(nil))
end
elseif obj == _G then
setter = function(value)
_parameterChanged()
return filter and filter(value) or value
end
else
setter = function(value)
value = filter and filter(value) or value
if first then first = nil; return end
obj[variable] = value
_parameterChanged()
return value
end
end
if cb then
return function(value)
if first then first = nil; return end
return cb(setter(value))
end
end
return setter
end
-- make a function to get the value of an object field
local function _makeGetter(obj, variable, filter)
if _ismetavariable(variable) then
return function(value)
local o, v, value = _metavariable(obj, variable)
if o and v then
return (filter and filter(o[v])) or o[v]
else
return (filter and filter(nil)) or nil
end
end
else
return function()
return (filter and filter(obj[variable])) or obj[variable]
end
end
end
-- update the parameter inspector
local function _update()
local doUpdate = true
if not _current then doUpdate = false end
local plist = __plist[_current]
if not plist then doUpdate = false end
if doUpdate then
for _, p in ipairs(plist) do
if p.get then _G[p.global] = p.get() end
end
else
_updater:stop()
end
end
-- callback to selector parameter;
-- also called when value is changed manually
local function _selectorChanged(v)
local plist, str = _plistIndex(v), ''
local object = plist and _findObject(plist.__id:get())
if object and plist then
local sid = tostring(plist.__id)
if _current then
local cplist = __plist[_current]
str = 'Inspecting: ' .. tostring(cplist.__id)
if plist ~= cplist then
str = str .. '\nSelect: ' .. sid
end
else
str = 'Select: ' .. sid
end
_setSelected(object)
else
_setSelected()
str = '(parameters deleted)\n'
end
_inspectorInfo = str
_setInspectorMessage()
_parameterChanged()
end
-- create inspector controls
local function _makeControls(plist, last)
if _reloader and _reloader.active then
_inspectorMessage = nil
_reloader:stop()
end
local plistCount, id = _plistCount(), plist and plist.__id or nil
if plistCount > 0 then
if _visible and last and (last == _current) then return end
local chooseInit
if _current then
chooseInit = _plistIndexOf(id or __plist[_current].__id)
parameter.clear()
else
chooseInit = 1
end
parameter.watch(_chooseWatchParam)
parameter.integer(_chooseParam, 1, plistCount, chooseInit, _selectorChanged)
if last then
local str = id and tostring(id) or tostring(_plistIndex(1).__id)
_inspectorInfo = 'Inspecting: ' .. str
_setInspectorMessage()
end
parameter.action(_selectParam, function()
local plist = _plistIndex(_G[_chooseParam])
if plist then
local object = _findObject(plist.__id:get())
if _current ~= object then
_removeGlobals()
tparameter.show(object)
return
end
end
if _reloader and _reloader.active then
_reloader:stop()
_inspectorMessage = nil
_setInspectorMessage()
tparameter.reload()
end
end)
if plist then
for _, p in ipairs(plist) do p.init() end
end
else
_inspectorInfo = 'nothing to inspect'
_inspectorMessage = nil
_setInspectorMessage()
parameter.watch(_chooseWatchParam)
end
_visible = true
end
-- inspect an object using a generic index
function _inspect(index)
local object = _index(index)
local plist = object and __plist[object]
if object and plist then
local last = _current
_setCurrent(object)
_makeControls(plist, last)
if not _updater then
_updater = Timer(_updateRate, 0, _update)
else
_updater:restart()
end
else
print('tparameter error: parameters for "',
index, '"not found')
end
end
-- return global proxy, set, and get functions
local function _tparam(name, obj, variable, filter, cb)
return _global(obj, name, variable),
_makeSetter(obj, variable, filter, cb),
_makeGetter(obj, variable, filter)
end
-- simulate a numeric text field for modifying number values
local function _numericText(name, obj, variable, filter, cb)
local global, set, get = _tparam(name, obj, variable, filter, cb)
local function init()
local first = true
parameter.text(global, get() or 0, function(v)
if first then first = nil; return end
set(v)
end)
end
_newParam(global, name, obj, variable, init, get)
end
--
-- tparameter interface
--
tparameter = setmetatable({}, {__call = function(_, ...)
tparameter.show(...)
end})
-- integer parameter
function tparameter.integer(name, obj, variable, min, max, cb)
min, max = min or 0, max or 10
local global, set, get = _tparam(name, obj, variable, nil, cb)
local function init()
local first = true
parameter.integer(global, min, max, get() or min, function(v)
if first then first = nil; return end
set(v)
end)
end
_newParam(global, name, obj, variable, init, get)
end
-- number parameter
function tparameter.number(name, obj, variable, min, max, cb)
min, max = min or 0, max or 1
local global, set, get = _tparam(name, obj, variable, nil, cb)
local function init()
local first = true
parameter.number(global, min, max, get() or min, function(v)
if first then first = nil; return end
set(v)
end)
end
_newParam(global, name, obj, variable, init, get)
end
-- boolean parameter
function tparameter.boolean(name, obj, variable, cb)
local global, set, get = _tparam(name, obj, variable, nil, cb)
local function init()
local first = true
parameter.boolean(global, get() or false, function(v)
if first then first = nil; return end
set(v)
end)
end
_newParam(global, name, obj, variable, init, get)
end
-- color parameter
function tparameter.color(name, obj, variable, cb)
local global, set, get = _tparam(name, obj, variable, nil, cb)
local function init()
local first = true
parameter.color(global, get() or color(), function(v)
if first then first = nil; return end
set(v)
end)
end
_newParam(global, name, obj, variable, init, get)
end
-- text parameter
function tparameter.text(name, obj, variable, cb)
local global, set, get = _tparam(name, obj, variable, nil, cb)
local function init()
local first = true
parameter.text(global, get() or '', function(v)
if first then first = nil; return end
set(v)
end)
end
_newParam(global, name, obj, variable, init, get)
end
-- numeric integer text pseudo-parameter
function tparameter.itext(name, obj, variable, min, max, cb)
min, max = min or 0, max or 100
local filter = function(value)
return _min(max, _max(min, _floor(tonumber(value) or 0)))
end
_numericText(name, obj, variable, filter, cb)
end
-- numeric text pseudo-parameter
function tparameter.ntext(name, obj, variable, min, max, cb)
min, max = min or 0, max or 100
local filter = function(value)
return _min(max, _max(min, tonumber(value) or 0))
end
_numericText(name, obj, variable, filter, cb)
end
-- calls an object's member function.
-- tparameter.method doesn't pass any arguments to the callback
-- (well, technically it sends 'self', except when obj is _G)
function tparameter.method(name, obj, variable, label)
if _ismetavariable(variable) then
error('tparameter: metavariables are not allowed with tparameter.action')
end
local call
if obj == _G then
call = function()
local c = obj[variable]
if c then c() end
end
else
call = function()
local c = obj[variable]
if c then c(obj) end
end
end
local init = function() parameter.action(label, call) end
_newParam(label, name, obj, variable, init)
end
-- call a plain function.
function tparameter.action(name, obj, label, cb, ...)
local init = function() parameter.action(label, cb) end
local global = label:gsub('%W', '_')
_newParam(global, name, obj, global, init)
end
-- watch parameters in long lists are crashy in currently (1.5.2)
-- workaround: use text parameter without a setter
function tparameter.watch(name, obj, variable)
-- the only way we can make a non-setting text parameter for _G
-- variable is to make it metavariable. We can do this because
-- _G can be accessed from _G
if obj == _G then
variable = '._G.' .. variable
end
local global = _global(obj, name, variable)
local get = _makeGetter(obj, variable)
local init = function() parameter.text(global, get() or '') end
_newParam(global, name, obj, variable, init, get)
end
-- show the object parameter inspector
function tparameter.show(index)
if index then
_inspect(index)
elseif _current then
_inspect(_current)
else
_makeControls()
end
end
-- hide the object parameter inspector
function tparameter.hide() _clear(true) end
-- clear the parameter inspector
tparameter.clear = _clear
-- reset inspector to unselected state
function tparameter.reset()
local visible = _visible
_clear()
if visible then tparameter.show() end
end
-- reload inspector with currently selected parameters
function tparameter.reload(index)
local current, visible = _current, _visible
_clear()
if visible then tparameter.show(index or current) end
end
-- call tparameter.delete(index) to delete
-- an object's parameters via generic index.
-- index may be an integer, parameter list id, or a table/object
-- if parameters are deleted, a reload will be triggered.
-- call tparameter:delete() to delete all parameters
function tparameter.delete(index)
-- delete all parameters
if index == tparameter then
local visible = _visible
_clear()
_setCurrent(nil)
for k in pairs(__plist) do __plist[k] = nil end
if visible then tparameter.show() end
return
end
local object = _index(index)
if not object then return end
local plist
if _selected == object then
if _plistCount() == 1 then
_setSelected()
elseif _current then
plist = __plist[_current]
_setSelected()
else
local i = _G[_chooseParam]
local newindex = (i == 1) and 2 or (i - 1)
plist = _plistIndex(newindex)
_setSelected()
end
elseif _selected then
plist = __plist[_selected]
end
local delayedReload = true
if _current and (_current == object) then
_setCurrent()
delayedReload = false
end
-- remove the object from our lists
__plistIndex[tonumber(__plist[object].__id:get())] = nil
__plist[object] = nil
if _plistIsEmpty() then
if _reloader and _reloader.active then _reloader:stop() end
tparameter.reload()
return
end
if plist then
local idx = _plistIndexOf(plist.__id)
_G[_chooseParam] = idx
_selectorChanged(idx)
end
if delayedReload then
_delayedReload()
else
tparameter.reload()
end
end
-- return the plist id and string containing pseudonyms for an object
function tparameter.id(obj)
local id = _plist(obj).__id
return id:get(), tostring(id)
end
-- register an object to be notified of inspector events.
-- events sent are 'highlighted' and 'selected'
function tparameter.notify(object, highlighted)
local plist = _plist(object)
if plist then
plist._selected = highlighted
end
end
-- update the inspector's timers
function tparameter.update(dt)
if _refresher and _refresh.active then _refresher:update(dt) end
if _updater and _updater.active then _updater:update(dt) end
if _reloader and _reloader.active then _reloader:update(dt) end
end
-- export to global environment
_G.tparameter = tparameter
--# Main
-- Main
-- TODO: set project info (author, etc)
-- call addEllipse() in the REPL to add defaultly constructed ellipse.
-- call removeEllipse() in the REPL to remove an ellipse from the array.
-- create a table to be used as an array
ellipses = {}
function addEllipse(...)
local e = TestEllipse(...)
ellipses[#ellipses+1] = e
return e
end
function removeEllipse(indexOrEllipse)
local removeIdx = indexOrEllipse
if type(indexOrEllipse) == 'table' then
for i, v in ipairs(ellipses) do
if v == indexOrEllipse then
removeIdx = i
break
end
end
end
local e = ellipses[removeIdx]
if e then
e:destroy()
table.remove(ellipses, removeIdx)
end
end
-- shortcuts for adding and removing ellipses in the REPL
a, r = addEllipse, removeEllipse
function setup()
-- tparameter works with _G too
tparameter.action('Global', _G, 'Hide Parameter Inspector', function()
tparameter.hide()
print('type tparameter() or tparamter.show() in the command window to re-enable')
end)
-- tparameter.method makes a special case when using _G:
-- it doesn't pass 'self' in (we don't need it to, since it's _G).
tparameter.method('Global', _G, 'addEllipse', 'Create New Ellipse')
tparameter.watch('Global', _G, 'ElapsedTime')
backgroundColor = color(0)
tparameter.integer('Global', _G, 'backgroundColor.r', 0, 255)
tparameter.integer('Global', _G, 'backgroundColor.g', 0, 255)
tparameter.integer('Global', _G, 'backgroundColor.b', 0, 255)
-- make this ellipse move back and forth repeatedly
local e = addEllipse(WIDTH/4, HEIGHT/2, color(255, 0, 255), 200)
tween(5, e, {y=0}, {loop=tween.loop.pingpong})
-- do a color cycle on the fill color
e = addEllipse(WIDTH*3/4, HEIGHT/2, color(255, 255, 0), 200)
tween(3, e.fillColor, {r=0, g=0, b=255}, {loop=tween.loop.pingpong})
-- modify an already created parameter after 2 seconds
AutoTimer(2, 1, function()
tparameter.itext('Ellipse', ellipses[1], 'dx', 0, WIDTH*2)
end)
-- show the parameter inspector.
-- this could also be called from the REPL
tparameter.show()
end
local accum = 0
function draw()
background(backgroundColor)
AutoTimer.update(DeltaTime)
for index, e in ipairs(ellipses) do
e:draw()
end
-- this *must* be the last thing you do in your draw function
tparameter.update(DeltaTime)
end
--# Timer
-- Timer
-- * TODO: allow creation of AutoTimer objects, so custom
-- auto timer lists can be created
--
-- * This code does not depend on Codea; it is generic Lua code.
-- * use Timer/TimerDT/TimerE to create a timer that must
-- be manually updated.
-- * use AutoTimer/AutoTimerDT/AutoTimerE to create a timer that is
-- updated whenever AutoTimer.update is called (usually in your main
-- draw function)
-- * use the DT variant to have timer pass an additional argument
-- containing the elapsed time since the callback was last fired,
-- or since the timer was started (TimerDT, AutoTimerDT)
-- * use the E variant to have the timer pass an additional argument
-- containing a table including data about the timer event.
-- Data includes:
-- - eventdata.timer - the timer invoking the callback
-- - eventdata.dt - the elapsed time since the callback was
-- invoked, or since the timer was started
-- - eventdata.n - the number of times this callback has
-- been called since the timer was started
--
local unpack, select = unpack, select
local mod, max = math.mod, math.max
local tinsert = table.insert
--
-- base timer interface
--
local _Timer = setmetatable({}, {__call = function(_Timer)
return setmetatable({__eventdata = {}}, _Timer)
end})
_Timer.__index = _Timer
function _Timer:__invokeCB(eventdata, params)
if params then
self.cb(unpack(params))
else
self.cb()
end
end
function _Timer:start(interval, iterations, cb, ...)
assert(interval and iterations and cb)
if not self.active and self._onstart then self:_onstart() end
self.active, self.accum, self.times = true, 0, 0
self.interval, self.cb = interval, cb
self.iterations = max(iterations, 0)
if select('#', ...) == 0 then
if self.params and self.params.reuse then
self.params.reuse = nil
else
self.params = nil
end
else
self.params = {...}
end
return self
end
function _Timer:restart(...)
local interval, iterations, cb =
self.interval, self.iterations, self.cb
if select('#', ...) == 0 and self.params then
self.params.reuse = true
self:start(interval, iterations, cb)
else
self:start(self.interval, self.iterations, self.cb, ...)
end
end
function _Timer:stop()
if not self.active then return end
self.active, self.accum, self.times = false, 0, 0
if self._onstop then self:_onstop() end
end
function _Timer:update(dt)
if not self.active then return end
local accum, interval = self.accum, self.interval
accum = accum + dt
if accum >= interval then
local stop = false
local e, iterations = self.__eventdata, self.iterations
while (not stop) and accum >= interval do
if iterations > 0 and self.times == iterations - 1 then
e.dt = accum
e.last = true
else
if accum < interval * 2 then
e.dt = accum
else
e.dt = interval + mod(accum, interval)
end
end
accum = accum - interval
self.accum = accum
self.times = self.times + 1
e.n = self.times
if iterations > 0 and self.times == iterations then
self:stop(); stop = true
end
local params = self.params
e.timer = self
self:__invokeCB(e, params)
e.timer, e.dt, e.n, e.last = nil
end
else
self.accum = accum
end
return self.active
end
--
-- alternate callback call signatures
--
local function __invokeCBE(self, eventdata, params)
if params then
self.cb(eventdata, unpack(params))
else
self.cb(eventdata)
end
end
local function __invokeCBDT(self, eventdata, params)
if params then
self.cb(eventdata.dt, unpack(params))
else
self.cb(eventdata.dt)
end
end
--
-- basic auto timer interface
--
local timers, add, remove, isUpdating = {}, {}, {}, false
local function addTimer(timer)
if not isUpdating then
timers[timer] = true
else
add[timer] = true
end
end
local function removeTimer(timer)
if not isUpdating then
timers[timer] = nil
else
remove[timer] = true
end
end
local function __onstart(self) addTimer(self) end
local function __onstop(self) removeTimer(self) end
local function _AutoTimer()
local t = _Timer()
t._onstart, t._onstop = __onstart, __onstop
return t
end
--
-- Timer API
--
Timer = function(...)
return _Timer():start(...)
end
TimerE = function(...)
local t = _Timer()
t.__invokeCB = __invokeCBE
return t:start(...)
end
TimerDT = function(...)
local t = _Timer()
t.__invokeCB = __invokeCBDT
return t:start(...)
end
--
-- AutoTimer API
--
AutoTimer = setmetatable({}, {__call = function(_, ...)
return _AutoTimer():start(...)
end})
AutoTimerE = function(...)
local t = _AutoTimer()
t.__invokeCB = __invokeCBE
return t:start(...)
end
AutoTimerDT = function(...)
local t = _AutoTimer()
t.__invokeCB = __invokeCBDT
return t:start(...)
end
-- call this every frame with the delta time
-- to update all active AutoTimers
function AutoTimer:update(dt)
dt, self = dt or self -- allow . or : calling syntax
isUpdating = true
for timer in pairs(timers) do timer:update(dt) end
isUpdating = false
for timer in pairs(remove) do
removeTimer(timer)
remove[timer] = nil
end
for timer in pairs(add) do
addTimer(timer)
add[timer] = nil
end
end
--# TestEllipse
-- Ellipse
TestEllipse = class()
-- defaults for newly created ellipses
local _defaults
local function _initDefaults()
_defaults = {
x = WIDTH/2,
y = HEIGHT/2,
fillColor = color(255),
hasStroke = false,
strokeWidth = 5,
strokeColor = color(255),
dx = 100,
dy = 100,
}
tparameter.integer('EllipseDefaults', _defaults, 'x', 0, WIDTH)
tparameter.integer('EllipseDefaults', _defaults, 'y', 0, HEIGHT)
tparameter.color('EllipseDefaults', _defaults, 'fillColor')
tparameter.boolean('EllipseDefaults', _defaults, 'hasStroke')
tparameter.integer('EllipseDefaults', _defaults, 'strokeWidth')
tparameter.color('EllipseDefaults', _defaults, 'strokeColor')
tparameter.integer('EllipseDefaults', _defaults, 'dx', 0, 1000)
tparameter.integer('EllipseDefaults', _defaults, 'dy', 0, 1000)
end
-- when an ellipse is highlighted in the inspector,
-- we will draw a larger ellipse around it with
-- this color. we use tween to constantly cycle
-- this color.
local _highlightColor = color(255, 192)
tween(0.5, _highlightColor,
{r=0, g=0, b=0, a=0},
{easing='sineIn', loop=tween.loop.pingpong})
-- do the same with the selected color
local _selectColor = color(255, 0, 0, 255)
tween(0.5, _selectColor,
{r=0, g=255},
{easing='sineIn', loop=tween.loop.pingpong})
function TestEllipse:init(x, y, fillColor, dx, dy)
-- create defaults and default parameters when the first ellipse is created
if not _defaults then _initDefaults() end
self.x = x or _defaults.x
self.y = y or _defaults.y
self.fillColor = fillColor or _defaults.fillColor
self.hasStroke = _defaults.hasStroke
self.strokeWidth = _defaults.strokeWidth
self.strokeColor = _defaults.strokeColor
self.dx = dx or _defaults.dx
self.dy = dy or dx or _defaults.dy
self.elapsed = 0
-- add parameters
tparameter.text('Ellipse', self, 'name')
tparameter.watch('Ellipse', self, 'elapsed')
tparameter.integer('Ellipse', self, 'x', 0, WIDTH)
tparameter.integer('Ellipse', self, 'y', 0, HEIGHT)
tparameter.number('Ellipse', self, 'dx', 0, 500)
tparameter.ntext('Ellipse', self, 'dy', 0, 500)
tparameter.integer('Ellipse', self, 'fillColor.r', 0, 255)
tparameter.integer('Ellipse', self, 'fillColor.g', 0, 255)
tparameter.integer('Ellipse', self, 'fillColor.b', 0, 255)
tparameter.itext('Ellipse', self, 'fillColor.a', 0, 255)
tparameter.boolean('Ellipse', self, 'hasStroke')
tparameter.color('Ellipse', self, 'strokeColor')
tparameter.integer('Ellipse', self, 'strokeWidth', 0, 1000)
tparameter.method('Ellipse', self, 'setDefaults', 'Set to default')
tparameter.action('Ellipse', self, 'Remove Ellipse', function()
removeEllipse(self)
end)
-- we can get our id from the parameter inspector
self.name = tparameter.id(self)
-- we want to be notified when the inspector
-- selects/deselects this object, so we can
-- update the drawing to reflect it.
-- this makes it much easier to find and tweak
-- the paramerts for the object we want.
tparameter.notify(self, function(event, selected)
if event == 'highlight' then
self._highlighted = selected
elseif event == 'select' then
self._selected = selected
end
end)
end
function TestEllipse:destroy()
tparameter.delete(self)
end
function TestEllipse:setPosition(x, y)
self.x = x
self.y = y
end
function TestEllipse:setDiameter(dx, dy)
self.dx = dx
self.dy = dy or self.dx
end
function TestEllipse:setDefaults()
self.x, self.y = _defaults.x, _defaults.y
self.fillColor = _defaults.fillColor
self.hasStroke = _defaults.hasStroke
self.strokeWidth = _defaults.strokeWidth
self.strokeColor = _defaults.strokeColor
self.dx, self.dy = _defaults.dx, _defaults.dy
self.name = tparameter.id(self)
end
function TestEllipse:draw()
if self._selected then
strokeWidth(10)
stroke(_selectColor)
fill(0, 0)
ellipse(self.x, self.y, self.dx + 20, self.dy + 20)
elseif self._highlighted then
strokeWidth(10)
stroke(_highlightColor)
fill(0, 0)
ellipse(self.x, self.y, self.dx + 20, self.dy + 20)
end
if self.hasStroke then
strokeWidth(self.strokeWidth)
stroke(self.strokeColor)
else
strokeWidth(0)
end
fill(self.fillColor)
ellipse(self.x, self.y, self.dx, self.dy)
fill(0)
text(self.name, self.x, self.y)
-- update elapsed time so we have something to watch
self.elapsed = self.elapsed + DeltaTime
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment