Last active
December 16, 2015 07:49
-
-
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# 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