open-source utilities and extras for Max/MSP
[ start | index | login ]
start > Software > net.loadbang.shado > README.MANUAL

README.MANUAL

Created by nick. Last edited by nick, one year and 232 days ago. Viewed 645 times. #3
[diff] [history] [edit] [rdf]
labels
attachments
S H A D O   M A N U A L

This file will eventually be a manual; for now, it's a working overview of the shado architecture and components, with some fragments of Python code which show how it works. Read in conjunction with the Python examples and the javadocs, it should be enough information to start writing shado-based applications.

- THE RENDERING MODEL -------------------

shado is a compositing and sprite library for the monome, written in Java. It is designed to be driven from a lightweight scripting language such as Python, and it uses a Java OSC library to talk to the monome via the MonomeSerial program.

(The Java OSC library is not yet used for input, for hysterical reasons; this will change in due course.)

An application which uses shado will build structures of "frames", "blocks" and "view ports". These objects know how to present themselves to a rendering object, which generates the OSC messages for MonomeSerial. The renderer is smart enough to send incremental updates for a display, to minimise network traffic and load.

The shado library doesn't have things called sprites per se, but any structure of blocks, frames and view ports can be manipulated and altered dynamically, in Python (say). If components are moved around relative to one another, or if their layering order or visibility are changed, the result is sprite-like animation.

- BLOCKS ------

A block is a two-dimensional matrix of data values representing monome lights, or pixels. Each block pixel value (a type we refer to as a "lamp") can have the obvious value ON or OFF; a lamp can also have the value THRU (which means "transparent": show whatever is beneath) or FLIP (invert whatever is beneath). It is possible to render a block directly to a monome - the block is considered to be sitting on a black base, so ON and FLIP cause the lamp to light, and OFF and THRU turn it off.

Here's a simple 4 x 4 square of lights (see the example code for all the details of the required import statements):

outputter = OSCOutputter('localhost', 8080, 8, '/m64') renderer = Renderer(8, 8, outputter) block = Block(4, 4).fill(LampState.ON) renderer.render(block)

Block(...).fill(...) sets all pixels in the block. Alternatively, single pixels can be set (via setLamp()). Block also has a constructor which takes an ASCII string representing pixel values (useful for quickly initialising those digital clock demos):

block = Block('111 101 101 101 111')

'0' means OFF, '1' means ON, '.' means THRU and '/' means FLIP. Each token is a row; the tokens are separated by white space.

- FRAMES ------

A frame is a stack of blocks, where items at the "top" obscure, or modify, items lower down. In fact, more generally, a frame is a stack of "renderables", which may be blocks, view ports (described later), or other frames. This recursive structure allows a complex animation (say) to be constructed in a frame, and then for this frame to be moved around inside an enclosing frame. (This is how the "UFO" animation sequence works.)

Unlike blocks and view ports, frames do not have dimensions; a frame's "size" can be considered to be defined by the extents of any blocks or view ports it, or its sub-frames, contain. The renderer does not care about the size of frames (or of blocks or view ports, for that matter): it knows the size of the monome it's addressing, and scans an area corresponding to the monome's width and height, asking renderables to deliver their data. (So, an obvious way to hide a block is to move it beyond the monome's coordinates; there are better ways, outlined below.)

A frame renders its pixel values from the bottom to the top of its list; higher items modify what is below. Lamp values of OFF and ON obscure underlying pixels; THRU passes pixels through unchanged, FLIP inverts them. The resulting pixel values in a frame still have these four lamp values, which are then interpreted by any parent frame as part of its stack of renderables, and so on to the actual frame which is rendered. The renderer asks this outermost frame or block to "fold" lamp values to ON or OFF (i.e., render them against black) prior to transmitting them.

The frame constructor has no arguments, so:

f = Frame() renderer.render(f)

is an easy way to clear a monome (an empty frame renders to black).

Renderables are added using add():

block = Block(.....) Frame.add(block, 2, 3)

This adds a renderable to the *top* of a frame, in this case offset horizontally by two pixels and vertically by three.

Frame.add() returns the frame itself, allowing cascading:

Frame.add(b1).add(b2)

A renderable can only be added to any frame once - this is because the renderable value is used in subsequent operations like show(), hide() and top(). It is possible to get replicated tiling effects by creating multiple unique sub-frames (or view ports) over a single block, and adding these to the main frame. (There's a simple demo in "Animate.py" which does this.)

A frame allows a few operation on its stack of renderables:

frame.top(item): bring a renderable to the top frame.bottom(item): send a renderable to the bottom frame.hide(item): hide a renderable (make it transparent) frame.show(item): show a renderable frame.remove(item): remove a renderable from a frame

And finally, some sprite action:

frame.moveBy(item, dx, dy): move a renderable by this distance frame.moveTo(item, x, y): move a renderable to this location relative to the frame's origin

Since the renderables might themselves be frames, all sorts of nested movement and animation is possible. In addition, hide() and show() (or, depending on style, bottom() and top()) can be used for animation: if you want to invert an entire monome, use a frame with a large

Block(...).fill(LampState.FLIP)

at the top; hide() and show() calls on it will invert everything. If you want to switch between a number of different patterns, create them all in advance, making sure they are all the same size and are opaque (ON or OFF values only, no THRU or FLIP) and them call frame.top() on them in sequence. (See Counter.py for an example.)

- VIEW PORTS ----------

ViewPorts provide a simple way to crop blocks or (more likely) frames, useful if animated sub-frames are being tiled into a larger system. When a view port is built around a renderable (a block or frame), the result is a port onto that renderable; anything outside the port is rendered as LampState.THRU (transparent). There is no change to the coordinate system of the contents of the port.

ViewPorts are also renderables, and so may be incorporated into frames, cropped in other ports, and so on.

After:

p = ViewPort(renderable, x, y, width, height)

the renderable "p" will be the same as `renderable' for pixels whose column is between x and x+width-1, and whose row is between y and y+height-1. Outside those coordinates, the pixels of "p" will be LampState.THRU.

ViewPort objects also expose properties "x", "y", "width" and "height", so that the cropping dimensions can be changed dynamically:

p.x = 3 p.height = p.height - 1

- BUTTON INPUT ------------

The machinery for dealing with button presses works with the same structures as those used to drive the LEDs. Once a structure of blocks, frames and view ports has been built to generate output, button presses can be routed into those same blocks, frames and view ports. The assumption is that an application which draws some kind of animated widget with a bit of scripting code will also want to capture button presses locally in that same portion of code, with sensible local coordinates, regardless of what else might be going on in the system at the time.

Here's how it works: the Block and ViewPort classes both implement an interface called IPressable in the Java world. This means they contain a method as follows:

public boolean press(int x, int y, int how);

The Block and ViewPort classes provide a method which does nothing; in order to respond to button presses, a Block or ViewPort must be sub-classed and this method overridden. This can obviously be done in Java, but it can also be done in Python.

When a button is pressed, shado searches a tree of renderables in order, until it finds one which handles the press; once the press is handled, the search stops. If the renderable returns true from the call to press(), then it is considered to have handled the event.

A Block or ViewPort can only handle a button press which falls within its coordinates; if the button press is outside the renderable's dimensions then the renderable never sees it.

(This, by the way, is why Frames cannot directly intercept button presses (they are not IPressables): a Frame does not have an obvious coordinate range.)

If a Block measuring (width * height) is within range of a press, it will be passed X and Y coordinates within (0, 0) and (width-1, height-1). If a ViewPort receives a press, the coordinate (0, 0) coincides with the top-left corner of the port, rather than (0, 0) in the port's coordinate system.

A button press can be routed into any renderable: Block, Frame or ViewPort. (Even though a Frame cannot handle presses directly, it will pass them on to its children.) There's a class called PressManager which does this (and which also tracks on and off presses, as we describe later):

f = Frame() … manager = PressManager(f) … manager.press(x, y, how)

(A PressManager can be built over a completely different structure to the one being displayed - but in most cases you probably don't want to do that.)

If the PressManager is constructed around a Block, the routing is simple: Block.press() gets called with the same coordinates that are passed in to PressManager.press() - these are presumably coming directly from the monome.

Sending a press to a ViewPort is slightly more complicated. The ViewPort might accept the event (by returning true from its press() method) in which case the event is considered finished. If the ViewPort returns false, the event is routed into the ViewPort's *content* - another renderable - with the original coordinates - and the result is whatever the content renderable returns.

When a press is routed to a Frame, the Frame starts calling into its stack of children in order, from top to bottom, mapping the coordinates so that each child sees (0, 0) as top-left. As soon as a child returns true, the event is over. If any child returns false, the Frame tries the next, and so on. If all children return false (or if the Frame is empty), the result is false.

Note that the handling of button presses by objects bears no relation to their visibility (but only to their coordinate range). An object which has been hidden (by Frame.hide()) will still be sent press() events. A Block which is completely transparent (all cell values are LampState.THRU) will also receive press() events. There are situations where this is useful: to capture the raw coordinates of a monome's buttons regardless of the objects in a frame, just add a monome-sized transparent layer to the top and use this to deal with the press() events.

Finally: a note about button presses and releases. If a button press is routed to an object deep within a visual heirarchy, then that structure can change dramatically before the button is released. For example, suppose that a Block receives a button press, and its press() method actually moves the Block within its enclosing Frame. The button release could have coordinates different to those of the press; or the release might be completely out of range of the new location of the Block.

We have implemented some machinery which guarantees a fundamental property of button handling: if a renderable receives - and handles - a button press at coordinates (x, y), then it will always receive the corresponding release at the same coordinates. It does not matter if the renderable has been moved out of range of the button - or even if the renderable has been completely removed from the object heirarchy - the PressManager keeps hold of it, purely so that the press(x, y, 0) can be sent to the original recipient of press(x, y, 1).

A side-effect of this is that, if an object chooses to ignore a press (by returning false from a press(x, y, 1) call) then it will never get the release call - that call will always go to the actual object which dealt with the press (if any).

Another side-effect is that an object might receive multiple button-on presses at the same coordinates. If a press(0, 0, 1) event to a Block causes it to move, another button on the monome might now map to the Block's top-left, and might send a second press(0, 0, 1). In other words, it's quite possible for button-on events to be duplicated in the same location - and the release events will also be duplicated. This makes perfect sense to the PressManager so it had better make sense to your Python scripts.

no comments | post comment



< March 2010 >
SunMonTueWedThuFriSat
123456
78910111213
14151617181920
21222324252627
28293031

XHTML 1.0 validated
CSS validated
RSS 2.0 validated
RSS Feed

Contact: Nick Rothwell

Powered by SnipSnap 1.0b3-uttoxeter

Open Source Java and UI Software for MaxMSP

ff194

The fireflash spanning router for the >>monome.

shado-small

The shado compositing and sprite library for the >>monome.


jython-logo

The Python programming language embedded in MaxMSP/MXJ


groovy-50

The Groovy programming language embedded in MaxMSP


small_powered_by

A Jetty-based embedded web server for MaxMSP, with JSP support


hsqldb

SQL database support, using HSQL or Apache Derby (embedded), or MySQL


nixie

Nixie tubes for Max


buttonsx

Rotating, alignable, colourable, assignable, pattr-aware arrays of text


Legacy Software

snipsnap.org | Copyright 2000-2002 Matthias L. Jugel and Stephan J. Schmidt