grid recipes
these snippets of code are starting points for some of the most common grid interactions, to help make incorporating grids into your scripts easier.
each are written following these “house” techniques:
- use flags to determine when to redraw the grid
- use tables to track flag states and grid coordinates
- query flag states at 30fps and only redraw when things change
these microstudies have been designed with simplicity and extensibility in mind. drop them into an existing script and stitch an interface together, or start from an interface and fill in functions as you go!
(grid GIFs created using @Tyler
’s excellent GridCapture library)
TOC
simple redraw
all keys are set up as switches, so only one can be lit at a time. press a new one, it lights up as the previous selection goes out. useful for state changes. use encoder 3 to change brightness level.
core concepts:
clock
-centric grid redrawgrid_dirty
flag to prompt grid redraw- introduce table elements (
show.x
andshow.y
instead of variables)
g = grid.connect() -- 'g' represents a connected grid
function init()
brightness = 15 -- brightness = full bright!
show = {x = 1, y = 1} -- table tracking x,y position
grid_dirty = true -- initialize with a redraw
clock.run(grid_redraw_clock) -- start the grid redraw clock
end
function grid_redraw_clock() -- our grid redraw clock
while true do -- while it's running...
clock.sleep(1/30) -- refresh at 30fps.
if grid_dirty then -- if a redraw is needed...
grid_redraw() -- redraw...
grid_dirty = false -- then redraw is no longer needed.
end
end
end
function grid_redraw() -- how we redraw
g:all(0) -- turn off all the LEDs
g:led(show.x,show.y,brightness) -- light this coordinate at indicated brightness
g:refresh() -- refresh the hardware to display the new LED selection
end
function g.key(x,y,z) -- define what happens if a grid key is pressed or released
if z==1 then -- if a grid key is pressed down...
show.x = x -- update stored x position to selected x position
show.y = y -- update stored y position to selected y position
grid_dirty = true -- flag for a redraw
end
end
function enc(n,d) -- define what happens in an encoder is turned
if n==3 then -- if encoder 3 is turned...
brightness = util.clamp(brightness + d,0,15) -- inc/dec brightness
grid_dirty = true -- flag for a redraw
end
end
try this:
- start the script with a random x,y position
- control brightness with a different encoder
- when a grid key is released, execute
print("key released!")
momentary keys
classic earthsea-style interaction. press a key and it lights up as it’s held. release to extinguish.
the difference between this snippet and the ‘simple redraw’ is that the state of every key is being independently tracked. this means that the state of each key won’t influence the others – instead of only one lit key at a time, you can press many keys at once and they’ll all light up.
core concepts:
- establish a table that holds booleans for every grid key
- utilize inline conditions (see line 41)
g = grid.connect() -- 'g' represents a connected grid
function init()
grid_dirty = false -- script initializes with no LEDs drawn
momentary = {} -- meta-table to track the state of all the grid keys
for x = 1,16 do -- for each x-column (16 on a 128-sized grid)...
momentary[x] = {} -- create a table that holds...
for y = 1,8 do -- each y-row (8 on a 128-sized grid)!
momentary[x][y] = false -- the state of each key is 'off'
end
end
clock.run(grid_redraw_clock) -- start the grid redraw clock
end
function grid_redraw_clock() -- our grid redraw clock
while true do -- while it's running...
clock.sleep(1/30) -- refresh at 30fps.
if grid_dirty then -- if a redraw is needed...
grid_redraw() -- redraw...
grid_dirty = false -- then redraw is no longer needed.
end
end
end
function grid_redraw() -- how we redraw
g:all(0) -- turn off all the LEDs
for x = 1,16 do -- for each column...
for y = 1,8 do -- and each row...
if momentary[x][y] then -- if the key is held...
g:led(x,y,15) -- turn on that LED!
end
end
end
g:refresh() -- refresh the hardware to display the LED state
end
function g.key(x,y,z) -- define what happens if a grid key is pressed or released
-- this is cool:
momentary[x][y] = z == 1 and true or false -- if a grid key is pressed, flip it's table entry to 'on'
-- what ^that^ did was use an inline condition to assign our momentary state.
-- same thing as: if z == 1 then momentary[x][y] = true else momentary[x][y] = false end
grid_dirty = true -- flag for redraw
end
try this:
- restrict momentary keys to a 64-sized grid
- restrict momentary keys to even-numbered rows
toggles
press an unlit key and it toggles on at half-bright. hold a half-bright key to make it full-bright. hold it again to return to half-bright. press a half-bright key to toggle it off.
here, we enter state management territory. instead of defining single-state toggles for each key, we use additional gesture information to switch between two toggle states: half-bright and full-bright. this is the foundation of modified behavior.
core concepts:
- using
clock
to track held time - managing
clock
state to cancel clocks with unmet criteria - modifying existing states
g = grid.connect()
function init()
grid_dirty = false
toggled = {} -- meta-table to track the state of the grid keys
brightness = {} -- meta-table to track the brightness of each grid key
counter = {} -- meta-table to hold counters to distinguish between long and short press
for x = 1,16 do -- for each x-column (16 on a 128-sized grid)...
toggled[x] = {} -- create an x state tracker,
brightness[x] = {} -- create an x brightness,
counter[x] = {} -- create a x state counter.
for y = 1,8 do -- for each y-row (8 on a 128-sized grid)...
toggled[x][y] = false -- create a y state tracker,
brightness[x][y] = 15 -- create a y brightness.
-- counters don't need futher initialization because they start as nil...
-- counter[x][y] = nil
end
end
clock.run(grid_redraw_clock)
end
function g.key(x,y,z)
if z == 1 then -- if a grid key is pressed...
counter[x][y] = clock.run(long_press,x,y) -- start the long press counter for that coordinate!
elseif z == 0 then -- otherwise, if a grid key is released...
if counter[x][y] then -- and the long press is still waiting...
clock.cancel(counter[x][y]) -- then cancel the long press clock,
short_press(x,y) -- and execute a short press instead.
end
end
end
function short_press(x,y) -- define a short press
if not toggled[x][y] then -- if the coordinate isn't toggled...
toggled[x][y] = true -- toggle it on,
brightness[x][y] = 8 -- set brightness to half.
elseif toggled[x][y] and brightness[x][y] == 8 then -- if the coordinate is toggled and half-bright
toggled[x][y] = false -- toggle it off.
-- we don't need to set the brightness to 0, because off LED will not be turned back on once we redraw
end
grid_dirty = true -- flag for redraw
end
function long_press(x,y) -- define a long press
clock.sleep(0.5) -- a long press waits for a half-second...
-- then all this stuff happens:
if toggled[x][y] then -- if key is toggled, then...
brightness[x][y] = brightness[x][y] == 15 and 8 or 15 -- flip brightness 8->15 or 15->8.
end
counter[x][y] = nil -- clear the counter
grid_dirty = true -- flag for redraw
end
function grid_redraw()
g:all(0)
for x = 1,16 do
for y = 1,8 do
if toggled[x][y] then -- if coordinate is toggled on...
g:led(x,y,brightness[x][y]) -- set LED to coordinate at specified brightness.
end
end
end
g:refresh()
end
function grid_redraw_clock()
while true do
if grid_dirty then
grid_redraw()
grid_dirty = false
end
clock.sleep(1/30)
end
end
try this:
- modify the script so that a long press on an unlit key toggles the key at full-bright (current behavior: only a short press can toggle an unlit key)
- modify the script so that a short press on full-bright key drops to half-bright (current behavior: only a long press on a full-bright key will drop to half-bright)
state machine
sorta like toggles, but a long press momentarily inverts the state of the key whereas a short press flips the state. very useful for alt menus + modifiers and doesn’t require dedicating any real-estate to single-purpose “meta” keys.
core concepts:
- implementing a low-level state machine
- using
clock
to track held time - managing
clock
state to cancel clocks with unmet criteria
g = grid.connect()
function init()
grid_dirty = true
toggled = {} -- meta-table to track the toggled state of each grid key
alt = {} -- meta-table to track the alt state of each grid key
counter = {}
for x = 1,16 do -- 16 cols
toggled[x] = {}
alt[x] = {}
counter[x] = {}
for y = 1,8 do -- 8 rows
toggled[x][y] = false
alt[x][y] = false
-- counters don't need futher initialization because they start as nil
end
end
clock.run(grid_redraw_clock)
end
function g.key(x,y,z)
if z == 1 then -- if a key is pressed...
counter[x][y] = clock.run(long_press,x,y) -- start counting toward a long press.
elseif z == 0 then -- if a key is released...
if counter[x][y] then -- if the long press counter is still active...
clock.cancel(counter[x][y]) -- kill the long press counter,
short_press(x,y) -- because it's a short press.
else -- if there was a long press...
long_release(x,y) -- release the long press.
end
end
end
function long_press(x,y)
clock.sleep(0.25) -- 0.25 second press = long press
alt[x][y] = true
counter[x][y] = nil -- set this to nil so key-up doesn't trigger a short press
grid_dirty = true
end
function long_release(x,y)
alt[x][y] = false
grid_dirty = true
end
function short_press(x,y)
toggled[x][y] = not toggled[x][y]
grid_dirty = true
end
function grid_redraw()
g:all(0)
for x=1,16 do
for y=1,8 do
if toggled[x][y] and not alt[x][y] then
g:led(x,y,15)
elseif alt[x][y] then
g:led(x,y,toggled[x][y] == true and 0 or 15)
end
end
end
g:refresh()
end
function grid_redraw_clock()
while true do
if grid_dirty then
grid_redraw()
grid_dirty = false
end
clock.sleep(1/30)
end
end
try this:
- short press = half-bright, long press = full-bright
- only during a long press, draw additional LEDs
- allow new toggles to be entered during a long press which are only redrawn during long presses
switches
foundation for a step sequencer – 16 columns of switches, stealing vertically.
core concepts:
- using nested tables to segment the grid display
g = grid.connect()
function init()
grid_dirty = true
switch = {}
for i = 1,16 do -- since we want rows to steal from each other, we only set up unique indices for columns
switch[i] = {y = 8} -- equivalent to switch[i]["y"] = 8
end
clock.run(grid_redraw_clock)
end
function grid_redraw_clock()
while true do
if grid_dirty then
grid_redraw()
grid_dirty = false
end
clock.sleep(1/30)
end
end
function grid_redraw()
g:all(0)
for i = 1,16 do
g:led(i, switch[i].y, 15)
end
g:refresh()
end
function g.key(x,y,z)
if z == 1 then
switch[x].y = y
grid_dirty = true
end
end
try this:
- add a moving playhead indicator
- incorporate a long press to modify and display an additional table of switches
range
hold a grid key and press another in the same row to establish a range. pressing a single key will establish a new start point for the range, so long as the entire range can fit. establishing a negative range resets to 1.
core concepts:
- using nested tables with multiple elements to track state
- using if/then conditions to define all possible interactions
g = grid.connect()
function init()
grid_dirty = true
range = {}
for i = 1,8 do
range[i] = {x1 = 1, x2 = 1, held = 0} -- equivalent to range[i]["x1"], range[i]["x2"], range[i]["held"]
end
clock.run(grid_redraw_clock)
end
function grid_redraw_clock()
while true do
clock.sleep(1/30)
if grid_dirty then
grid_redraw()
grid_dirty = false
end
end
end
function grid_redraw()
g:all(0)
for y = 1,8 do
for x = range[y].x1, range[y].x2 do
g:led(x,y,15)
end
end
g:refresh()
end
function g.key(x,y,z)
if z == 1 then
range[y].held = range[y].held + 1 -- tracks how many keys are down
local difference = range[y].x2 - range[y].x1
local original = {x1 = range[y].x1, x2 = range[y].x2} -- keep track of the original positions, in case we need to restore them
if range[y].held == 1 then -- if there's one key down...
range[y].x1 = x
range[y].x2 = x
if difference > 0 then -- and if there's a range...
if x + difference <= 16 then -- and if the new start point can accommodate the range...
range[y].x2 = x + difference -- set the range's start point to the selectedc key.
else -- otherwise, if there isn't enough room to move the range...
-- restore the original positions.
range[y].x1 = original.x1
range[y].x2 = original.x2
end
end
elseif range[y].held == 2 then -- if there's two keys down...
range[y].x2 = x -- set an range endpoint.
end
if range[y].x2 < range[y].x1 then -- if our second press is before our first...
range[y].x2 = range[y].x1 -- destroy the range.
end
elseif z == 0 then -- if a key is released...
range[y].held = range[y].held - 1 -- reduce the held count by 1.
end
grid_dirty = true
end
try this:
- add a moving playhead indicator in each row
- incorporate a long press to reset the range to 1
meters
16 vertical meters, use E3 to change height. great for step sequencers or showing parameter + variable states.
core concepts:
- more advanced table nesting + methods
- reverse
for
count g:led
features in-line comparison
g = grid.connect()
function init()
grid_dirty = true
meter = { {} , selected = 1 }
for x = 1,16 do
meter[x] = {height = 0}
end
clock.run(grid_redraw_clock)
end
function grid_redraw_clock()
while true do
clock.sleep(1/30)
if grid_dirty then
grid_redraw()
grid_dirty = false
end
end
end
function grid_redraw()
g:all(0)
for x = 1,16 do
for y = 8,8-meter[x].height,-1 do
g:led(x,y,meter.selected == x and 15 or 7)
end
g:led(meter.selected,8,15)
end
g:refresh()
end
function g.key(x,y,z)
if z == 1 then
meter.selected = x
end
grid_dirty = true
end
function enc(n,d)
if n == 3 then
meter[meter.selected].height = util.clamp(meter[meter.selected].height+d,0,7)
grid_dirty = true
end
end
try this:
- add a moving playhead indicator across the grid which runs across the top-most key in each column
- add a mechanism to the bottom row which dynamically sets a range to restrict the playhead’s movement
- add long presses as a way to toggle between two different meter states