-
Notifications
You must be signed in to change notification settings - Fork 21
UI Object Overview
An UI object controls the relationship between one or more Graphical Elements and a Graphical Wrapper. Graphical Elements are the building blocks of KievII: they can receive events and react to them, mantain and change an internal state, and they know how to draw themselves on a Graphical Wrapper.
The Graphical Wrapper hides the low level graphical details and provides a set of primitives that are used by the Elements to actually display themselves on the underlying graphical context.
I will use recursiontest as an example for this tutorial.
First of all, we have to create a Graphical Wrapper object and an UI object, then associate the first to the latter:
var plugin_canvas = document.getElementById("plugin");
var CWrapper = K2WRAPPER.createWrapper("CANVAS_WRAPPER",
{canvas: plugin_canvas}
);
ui = new UI (plugin_canvas, CWrapper);
The wrapper is a CANVAS_WRAPPER
. This means that every element that we'll add to our UI object will be rendered on an HTML5 canvas (we got the canvas element from the document object via document.getElementById
).
Notice also that we pass the same canvas object to the UI constructor. When you pass a DOM element to an UI object, the UI object adds some event listeners to the target. Current UI implementation adds listeners for mousedown
, mouseup
and mousemove
events.
Now we can create an array of three Knob Elements to populate the UI object:
var NKNOBS = 3
for (var i=0; i < NKNOBS; i+=1) {
knobArgs[i] = {
ID: "knob" + i,
top: 100,
left: i * 100,
imagesArray : loaderStatus.imagesArray,
sensivity : 5000,
onValueSet: function (slot, value, ID) {console.log (slot, value, ID); ui.refresh()}
};
knob[i] = new Knob(knobArgs[i]);
ui.addElement(knob[i], {zIndex: 10});
ui.setValue ({elementID: knobArgs[i].ID,
value: 0});
}
knobArgs[i]
is the variable in the array that we will repeatedly pass to the Knob()
constructor in order to build three Knob
elements.
Its ID
field is the unique ID of the element in the UI. No more than one element in an given UI instance can have the same ID (if you try to add two elements with the same ID, an exception is thrown).
top
and left
are the top, left position of the element in pixels, relative to the canvas topleft origin.
imagesArray
is the image array relative to the Knob element, ordered from the frame associated with the lowest value (0) to the frame associated with the highest (1).
I loaded the images via a loadImageArray
object from the Utilities section in this example, but as soon as they are completely loaded when you pass them to the constructor, they can be loaded in any way you please.
A more sophisticated kind of knob with one only image that rotates is available, but for this demo we will use a simple, multi-frame Knob.
sensitivity
is how much the knob is sensitive to the mouse movement when it is clicked upon and dragged up and down.
onValueSet
is very important: it's the function that is called back when the element state is changed (for example, when a Knob is dragged up or down with the mouse). It normally accepts three parameters: slot
, value
and ID
.
-
slot
is always "knobvalue" in a Knob, since a Knob is a single-slot element (so to speak, it has only to remember the knob's state, a value from 0 to 1). Other elements could have more than one slot (think for example about a dialog that makes you draw a Beziér curve: it has to have at least three indipendent control points). -
value
is the value the element assumed: if you drag the Knob down to its lowest point, for example, thevalue
parameter received by the callback function will be 0, if you drag it to the middle pointvalue
will be 0.5 and so on. -
ID
is the ID of the element in the UI that changed its state (this is particularly useful when we have a lot of elements and a single callback function to handle them all).
In this case, the callback doesn't do much: it just writes the parameters it received on the log console and it calls refresh()
. Calling refresh()
redraws the elements in the UI: redrawing is not done automatically whenever an element's state is changed, so you probably want to call refresh()
at the end of a callback if you don't want your element to look "frozen" (there are situations in which you don't want to refresh as soon as your element's state is changed, but this simple example is not one of them).
addElement()
adds the element to the UI. When your element is added to an UI, you can set its value and tell the UI how it must be drawn in respect to the other elements (not only: you can tell the UI if the element is active or inactive, or visible or invisible, for example).
addElement()
has 2 parameters: one is the element previously created with its constructor and arguments, the other is an optional object that specifies extra detail about the element. Currently, this optional object supports only one parameter: zIndex
.
Finally, setValue()
is a function that makes you set the value (= the internal state) of an element associated to a given UI. setValue()
has a number of options, but only elementID
(the unique ID associated to the element) and value
(the value to set) are mandatory. Here we set the initial value of the three knobs to 0.
zIndex
is a value >= 0 that represents the "depth" of a particular graphic element in the UI: the bigger zIndex
is, the more that particular graphic element is "on top" (towards the user). This affects the way UI refreshes the elements when refresh()
is called.
In particular, when refresh()
is called, all elements are re-drawn in their zIndex order, from the lowest to the highest.
So, if two elements overlap, the one with the bigger zIndex
value will be drawn top of the other (if you assign the same zIndex
to two overlapping elements, there is no safe way to predict what element will be drawn on top of the other).
If an element does not define its zIndex
, zIndex
is set to 0 by default. If zIndex is defined, but it is not a number or it's a number < 0, an exception is thrown.
Since in this example the three knobs don't overlap, we can assign the same zIndex value (10) to each one of them. Since they're in top of nothing and nothing overlaps them, we could have omitted completely the {zIndex: 10}
parameter.
Now, we want to chain our three knobs: when the user changes the value of one of them with the mouse, we want the others to change value as well, and we also want to tell each element how to change value in relation to the others' value.
In particular, here's what we want:
- Everytime Knob 0 changes its knobvalue, Knob 1 has to change its knobvalue automatically. But we want Knob 1 to be the inverse of Knob 0: when Knob 0 increases, Knob 1 decreases, and vice versa. This has to be true whether the value-changing action (e.g. the user manipulating the knob) is started by Knob 0 or by Knob 1.
- Similarly, everytime Knob 1 changes its knobvalue, Knob 2 has to change automatically. But we want Knob 2's range to be [0,0.5] instead of [0,1].
- To close the loop, we want Knob 2 to change the value of Knob 0. This time, Knob 0's value must be the equal to Knob 2's.
Using UI.connectSlots()
and the filter callback, we can accomplish this in four lines of code:
// 0 -> 1
ui.connectSlots("knob0", "knobvalue", "knob1", "knobvalue", {callback: function (value) {return 1-value;}});
// 1 -> 0
ui.connectSlots("knob1", "knobvalue", "knob0", "knobvalue", {callback: function (value) {return 1-value;}});
// 1 -> 2
ui.connectSlots("knob1", "knobvalue", "knob2", "knobvalue", {callback: function (value) {return value * 0.5;}});
// 2 -> 0
ui.connectSlots("knob2", "knobvalue", "knob0", "knobvalue", {callback: function (value) {return value;}});
UI.connectSlots takes five parameters. Its signature is:
UI.connectSlots = function (senderElement, senderSlot, receiverElement, receiverSlot, connectParameters)
senderElement
and senderSlot
are strings. They specify where the chain we want to add "starts".
Similarly, receiverElement
and receiverSlot
are strings, too. They specify where the chain we want to add "ends".
Calling UI.connectSlots means "When the value of slot senderSlot
of the element senderElement
changes, make the value of slot receiverSlot
in the element receiverElement
change accordingly".
The fifth parameter is an object containing optional parameters. The one that we need for now is the callback
one.
callback
is a callback function that acts as a filter when the UI object activates a particular connection. This callback simply takes the value of the senderSlot
as its only parameter, and returns the value that should be put in the receiverSlot
.
We used this callback to invert the knob value between Knob 0 -> Knob 1 and viceversa:
{callback: function (value) {
return 1-value;
}
};
But we can use it to make an hypothetical element with numeric values (a [RotKnob](RotKnob Element Interface) or a [Slider](Slider Element Interface), for example) to automatically change a slot value of another element which handles strings (for example a Label), or any other complex-as-you-like filter / translation you may need.
Slot connections is a directed graph, and chaining slots can introduce loops. If Knob 0 changes Knob 1, Knob 1 changes Knob 2 and Knob 2 changes Knob 0 in turn, won't this configuration generate an endless feedback between knobs?
The UI object "breaks" these infinite chains recursively storing the history of every changed element:slot pair. When an element:slot is seen for the second time in the same chain, UI knows the loop is endless and breaks the chain. This way, if you modify the value of the first knob (either with setValue()
or by using the mouse), the change is propagated to the second and the third knob, but not "fed back" again to the first knob, that is the change originator.