In the last two tutorials, we’ve looked at some of the possibilities and advantages of using Jitter objects and matrices within JavaScript code. In this tutorial we’ll encapsulate an entire Jitter OpenGL patch inside of JavaScript, using many of the techniques we’ve already seen. Most importantly, however, we’ll learn how to “listen” to the output of Jitter objects that have been encapsulated in JavaScript in order to design functions that respond interactively to these objects.
A number of Jitter objects (such as
jit.qt.movie,
jit.pwindow, and
jit.window) output messages from their status (right-hand) outlets in response to processes initiated by the user in a Max patcher. Fore example, when you read a movie file into
jit.qt.movie, the object outputs the message
read followed by the movie’s filename and a status number out its status outlet. Similarly, the
jit.pwindow and
jit.window objects can respond to mouse events in their windows by sending messages out their status outlet. Because Jitter objects instantiated within JavaScript have no inlets and outlets
per se, we need to explicitly create an object (called a JitterListener) to catch those messages and call a function (called a “callback” function) in response to them.
This Tutorial assumes you’ve looked at the earlier JavaScript tutorials, as well as the first OpenGL tutorial Tutorial 30: Drawing 3D Text.
• Open the tutorial patch
47jJitterListener in the Jitter Tutorials folder.
The first thing we notice about this tutorial patch is that there is very little in it, in terms of Max objects. Virtually all the work done in the patcher is accomplished inside the
js object in the patch.
Our Tutorial patcher: not much to look at.
• Click on the
toggle box labeled
Display. The
qmetro object will begin to send
bang messages into the
js object, and the
jit.window object should fill with a number of shapes (one large sphere and a few dozen small spheres) on a black background.
Our
js object contains a complete set of Jitter objects performing an OpenGL rendering (including a
jit.window object).
• Move your mouse over the
jit.window. The large sphere will “glue” to your mouse, following you along in the window. As the large sphere touches the smaller spheres, it will “push” them along as if they were solid objects colliding. Try to get a feel for moving the smaller spheres around with the larger one.
Our
js file not only draws our OpenGL scene, but also handles interactive events from the mouse. This is done through a listening and callback mechanism (called a JitterListener) that we’ll learn about in the JavaScript code.
Pushing the spheres around.
• Double-click the
js object in our Tutorial patch. A text editor will appear that contains the source code for the
js object in the patch. The code is saved as a file called ‘47jMarbles.js’ in the same folder as the Tutorial patch. Look at the global code block at the top of the file.
Our JavaScript code creates all the JitterObject objects we need to render our scene: a
jit.window object to display the drawing context, a
jit.gl.render object to perform the rendering, and a number of
jit.gl.gridshape objects to draw and animate the spheres in the scene. These objects are instantiated in the global block of code at the beginning of the JavaScript file.
var OBJECT_COUNT = 32; // number of little spheres
This line declares a variable (OBJECT_COUNT) that determines the number of smaller spheres in our scene.
// create a [jit.window] object for our display
// (this is the object we'll "listen" to):
var mywindow = new JitterObject("jit.window","ListenWindow");
// turn off depth testing... we're using blending instead:
mywindow.depthbuffer = 0;
// turn ON idlemousing... we want to listen for it:
mywindow.idlemouse = 1;
Next, we create a
jit.window object to display everything. We’ll refer to it in our JavaScript code by the variable
mywindow. Just as in a Max patch, the
jit.window needs a name (“ListenWindow”) that must match the OpenGL drawing context we’re using. We’ve turned off the
depthbuffer and turned on the
idlemouse attribute, which allows the
jit.window object to track mouse events in the window regardless of whether we’ve clicked our mouse down or not.
// create a [jit.gl.render] object for drawing into our window:
var myrender = new JitterObject("jit.gl.render","ListenWindow");
// use a 2-dimensional projection:
myrender.ortho = 2;
// set background to black with full erase opacity (no trails):
myrender.erase_color = [0,0,0,1];
Our
jit.gl.render object (assigned to the variable
myrender) defines a drawing context called “ListenWindow”. The
jit.window object (above) and the
jit.gl.gridshape objects (below) share this name. We’ve decided to use an
orthographic projection (by setting the
ortho attribute to
2). This means that the
z axis of our drawing context is ignored in terms of sizing objects based on their distance from the virtual camera. This essentially creates a 2-dimensional rendering of our scene. We’ve set the background to black (with a full erase between successive drawing) by setting our
erase_color to
0 0 0 1.
// create a [jit.gl.gridshape] object for use to control with the mouse
var mywidget = new JitterObject("jit.gl.gridshape","ListenWindow");
mywidget.shape = "sphere";
mywidget.lighting_enable = 1;
mywidget.smooth_shading = 1;
mywidget.scale = [0.1,0.1,0.1];
mywidget.color = [1,1,1,0.5] ;
mywidget.blend_enable = 1;
mywidget.position = [0,0]; // no z necessary in orthographic projection
The first
jit.gl.gridshape object we create (assigned to the variable
mywidget) corresponds to the large sphere moved by our mouse in the
jit.window. After creating it, we set all the relevant attributes we want the sphere to have, just as we would in a Max patcher. Note that in setting the
position attribute we only use two arguments (
x and
y). In an orthographic projection the
z axis is irrelevant to how the shapes are drawn.
// create an array of [jit.gl.gridshape] objects
// randomly arrayed across the window
var mysphere = new Array(OBJECT_COUNT);
Rather than naming our smaller spheres individually, we’re treating them as an Array (called
mysphere). Each element in this array will then be a separate
jit.gl.gridshape object that we can access by array notation (e.g.
mysphere[5] will be the
sixth sphere, counting up from 0).
// initialize our little spheres with random colors and positions (x,y)
for(var i="0;i<OBJECT_COUNT;i++)" {
mysphere[i] = new JitterObject("jit.gl.gridshape","ListenWindow");
mysphere[i].shape = "sphere";
mysphere[i].lighting_enable = 1;
mysphere[i].smooth_shading = 1;
mysphere[i].scale = [0.05,0.05,0.05];
mysphere[i].color = [Math.random(),Math.random(),Math.random(),0.5] ;
mysphere[i].position = [Math.random()*2.-1, Math.random()*2.-1];
mysphere[i].blend_enable = 1;
}
This code actually creates the individual
jit.gl.gridshape objects as individual JitterObjects that are members of the Array
mysphere. We use a JavaScript for() loop to set the initial attributes of all these objects in one block of code. We give them random initial
color and
position attributes, scattering them over the screen and coloring them all differently.
// create a JitterListener for our [jit.window] object
var mylistener = new JitterListener(mywindow.getregisteredname(), thecallback);
Finally, we create a variable called
mylistener that we assign to be a JitterListener object. JitterListener objects take two arguments: the
object that they “listen” to, and the
function that will be called when the object triggers an event. Our JitterListener object is set to listen to our
jit.window object (
mywindow). The getregisteredname() property of a JitterObject object returns the
name by which that object can be accessed by the JitterListener (in the case of
jit.window objects, this will be the same as name of the drawing context). Whenever our
jit.window object generates an event, a function called thecallback() will be triggered in our JavaScript code. Now that we’ve instantiated a JitterListener, we can (in most cases) leave it alone and simply deal with the mechanics of the callback function it triggers in response to an event from the object it listens to.
• Back in the Tutorial patcher, click the
message box labeled
randomize or hit the
spacebar on your computer keyboard. The small spheres should scatter and change colors. Look at the code for the randomize() function in our
js file.
function randomize() // reorient the little spheres
{
for(var i="0;i<32;i++)" {
mysphere[i].color = [Math.random(),Math.random(),Math.random(),0.5];
mysphere[i].position = [Math.random()*2.-1, Math.random()*2.-1];
}
}
Our randomize() function takes our little spheres and scatters them randomly, changing their colors. This allows us to basically restart our simulation whenever we like.
The
jit.window object responds to a variety of mouse events. When the
idlemouse attribute is set to
1 (as it is in our code) the
jit.window object outputs mouse information (the
mouseidle message) when you drag the pointer over the window. It also tells us when the pointer has left the window area (the
mouseidleout message). Regardless of the setting of the
idlemouse attribute, the
jit.window object also responds to mouse clicks inside the window (the
mouse message).
• In the Tutorial patcher, move the pointer into the
jit.window and click the mouse. The white sphere will turn into a green torus. Release the mouse and the torus will turn into a red sphere. Moving the pointer will turn the sphere white again.
How our sphere responds to mouse click events (mouse down and mouse up).
• Look at the code for the function called thecallback() in our JavaScript code. This code executes whenever our JitterListener object receives an event from our
jit.window object.
function thecallback(event)
// callback function to handle events triggered by mousing
// in our [jit.window]
{
var x,y,button; // some local variables
The
event constructor stores the message sent by the
jit.window object to the JitterListener object in our patch. We also declare a few local variables (
x,
y, and
button) to store information about the pointer derived from the
event message’s arguments.
We then use JavaScript if() statements based on the eventname property of the event that triggered the callback function. The eventname property will contain the name of the message (mouse, mouseidle, etc.) that triggered the function.
if (event.eventname="="mouse")" {
// we're entering, dragging within, or leaving a "mouse click" event
This part of the code handles a mouse click event (a click down, dragging of the mouse, and the release of a click).
// arguments are (x,y,button,cmd,shift,capslock,option,ctrl)...
// we only care about the first three
x = event.args[0];
y = event.args[1];
button = event.args[2];
The first three arguments to the mouse event contain the position of the mouse (x and y) and a flag telling us whether the mouse button is down (1) or up (0). We store these settings into local variables (x, y, and button) for use later on.
if (button) // we're clicked down
{
mywidget.color = [0,1,0,1]; // color our control object green
mywidget.shape = "torus"; // change it to a donut shape
}
else // we've just unclicked
{
mywidget.color = [1,0,0,1]; // color our object red
mywidget.shape = "sphere"; // change back to a sphere
}
}
If our button is down, we turn our
jit.gl.gridshape object
mywidget from a white sphere into a green torus. When we release the mouse, we turn it into a red sphere.
else if (event.eventname="="mouseidle")" {
// we're mousing over the window with the mouse up
x = event.args[0];
y = event.args[1];
mywidget.color = [1,1,1,1] ; // color our object white
}
This code executes whenever we drag our pointer across the
jit.window object without clicking the pointer. All we do here is store the variables
x and
y based on the pointer position. We also turn our
jit.gl.gridshape object (
mywidget) white, in case the previous callback had turned it a different color.
else if (event.eventname="="mouseidleout")" {
// we're no longer mousing over the window
x = event.args[0];
y = event.args[1];
mywidget.color = [1,1,1,0.5] ; // make our object translucent
}
When our pointer leaves the
jit.window, the
eventname property of our callback will be “mouseidleout”. We treat this the same as the “mouseidle” event. If we wanted to, we could treat it differently (e.g. make the white sphere disappear, or randomize the spheres).
// move our control object to the drawing context's
// equivalent of where our mouse event occurred:
mywidget.position = myrender.screentoworld(x,y);
Finally, we update the position of the
jit.gl.gridshape object controlled by our mouse by updating the
position attribute of the JitterObject object
mywidget. Because our drawing context works in OpenGL world coordinates (where the default center of the space is (0, 0) and positions are expressed in decimal values), we need to convert our
x and
y values into numbers that make sense for our drawing context. The screentoworld() method to a
jit.gl.render object will convert those values from pixel coordinates on the display to world coordinates for the rendering.
}
// don't allow this function to be called from Max
thecallback.local = 1;
Because our thecallback() function is intended to be executed by our JitterListener object, we set its
local property to
1 to prevent it from being executed directly by a Max message sent to the
js object.
Now that we’ve seen how to respond to mouse events and update our
mywidget object’s position, color, and shape accordingly, we need to look at how we perform the actual rendering of our scene in response to a
bang message from the
qmetro object in our Max patcher. The bang() function in our JavaScript code handles the drawing as well as the animation of the little spheres in response to the new position of our large sphere.
• Look at the code for the bang() function.
The majority of our bang() function iterates through the positions of all the little spheres and compares them to the position of the large one. If the distance between them is small enough that the spheres would overlap when drawn, we move the little sphere away the minimum amount necessary to have the spheres touch. This type of processing is known as collision detection and is a ubiquitous technique in the field of computer animation.
function bang()
// main drawing loop... figure out which little spheres to move
// and drive the renderer
{
// collision detection block. we need to iterate through
// the little spheres and check their distance from the control
// object. if we're touching we move the little sphere away
// along the correct angle of contact.
for(var i = 0;i<OBJECT_COUNT;i++) {
We enter a JavaScript for() loop to check the position of our large sphere against every small sphere, one at a time.
// cartesian distance along the x and y axis
var distx = mywidget.position[0]-mysphere[i].position[0];
var disty = mywidget.position[1]-mysphere[i].position[1];
// polar distance between the two objects
var r = Math.sqrt(distx*distx+disty*disty);
// angle of little sphere around control object
var theta = Math.atan2(disty,distx);
By subtracting the x and y positions of the two spheres (large and small) from one another, we get their Cartesian distance. We can convert this to polar (absolute) distance by applying the Pythagorean theorem to derive the hypotenuse (stored in the variable r). We then derive the angle of the small sphere around the large one by taking the arctangent of the rise (x) over the run (y).
// check for collision...
if(r<0.15)
// control object is size 0.1, little spheres are 0.05,
// so less than 0.15 and it's a hit...
{
Because our large sphere has a scale attribute of 0.1 0.1 0.1 and our small spheres have scale attributes of 0.05 0.05 0.05, we can infer that the objects are overlapping if the distance between them is less than 0.15. This triggers a block of code that deals with the collision.
// convert polar->cartesian to figure out x and y displacement
var movex = (0.15-r)*Math.cos(theta);
var movey = (0.15-r)*Math.sin(theta);
// offset the little sphere to the new position,
// which should be just beyond touching at the
// angle of contact we had before. the result
// should look like we've "pushed" it along...
mysphere[i].position = [mysphere[i].position[0]-movex, mysphere[i].position[1]-movey];
}
}
We use the polar distance and angle, combined with the minimum allowable distance between the objects (0.15), to derive how far we need to “nudge” the little sphere along the x and y axes to stop it from overlapping the large sphere. We figure out these values by converting the polar coordinates back into Cartesian values stored in the variables movex and movey. We then subtract these two variables’ values from the current position of the small sphere, pushing it out of the way.
// rendering block...
myrender.erase(); // erase the drawing context
myrender.drawclients(); // draw the client objects
myrender.swap(); // swap in the new drawing
}
This last block of code does the actual rendering. Just as you would when working with Jitter OpenGL objects in a Max patcher, you send an
erase message to the
jit.gl.render object (
myrender). The drawclients() method to
jit.gl.render collects all the relevant information from the OpenGL objects attached to our drawing context and draws them; any OpenGL objects with an
automatic attribute set to
0 will have to be drawn manually here. The swap() method then replaces the old rendering with the new one in the
jit.window object, showing us the updated scene. The drawclients() and swap() method are combined into one operation when you send a
bang message to a
jit.gl.render object in a Max patcher.
• Back in the Tutorial patcher, click the
toggle box attached to the
message box labeled
fullscreen $1 (or hit the
escape key on your computer keyboard). The
jit.window object will fill the screen. Notice that the coordinate conversion that allows you to move the large sphere around the screen with the mouse still works fine. You can take the
jit.window object out of fullscreen mode by pressing the
escape key on your keyboard. In the JavaScript code, look at the fullscreen() function:
function fullscreen(v)
// function to send the [jit.window] into fullscreen mode
{
mywindow.fullscreen = v;
}
The fullscreen() attribute of a
jit.window JitterObject object behaves just as the
fullscreen message sent to a
jit.window object in a Max patcher.
An entire OpenGL Jitter patch can be encapsulated in JavaScript by instantiating
jit.gl.render,
jit.window, and other OpenGL objects within procedural code written for the
js object. These objects have all their messages and attributes exposed as corresponding methods and properties. You can use a JitterListener object to respond to events triggered by a Jitter object within JavaScript. The JitterListener then executes a callback function, passing the calling message to it as its argument. This allows you to write JavaScript functions to respond to mouse interactivity in a
jit.window object, file reading in a
jit.qt.movie object, and other situations where you would want to respond to an event triggered by a message sent by a Jitter object out its status outlet in a Max patcher.
// 47jMarbles.js
//
// Demonstrates how to set up a JitterListener object to listen
// to the output of a Jitter object instantiated within [js].
//
// rld, 7.05
//
var OBJECT_COUNT = 32; // number of little spheres
// create a [jit.window] object for our display
// (this is the object we'll "listen" to):
var mywindow = new JitterObject("jit.window","ListenWindow");
// turn off depth testing... we're using blending instead:
mywindow.depthbuffer = 0;
// turn ON idlemousing... we want to listen for it:
mywindow.idlemouse = 1;
// create a [jit.gl.render] object for drawing into our window:
var myrender = new JitterObject("jit.gl.render","ListenWindow");
// use a 2-dimensional projection:
myrender.ortho = 2;
// set background to black with full erase opacity (no trails):
myrender.erase_color = [0,0,0,1];
// create a [jit.gl.gridshape] object for use to control with the mouse
var mywidget = new JitterObject("jit.gl.gridshape","ListenWindow");
mywidget.shape = "sphere";
mywidget.lighting_enable = 1;
mywidget.smooth_shading = 1;
mywidget.scale = [0.1,0.1,0.1];
mywidget.color = [1,1,1,0.5] ;
mywidget.blend_enable = 1;
mywidget.position = [0,0]; // no z necessary in orthographic projection
// create an array of [jit.gl.gridshape] objects randomly arrayed across the window
var mysphere = new Array(OBJECT_COUNT);
// initialize our little spheres with random colors and positions (x,y)
for(var i="0;i<OBJECT_COUNT;i++)" {
mysphere[i] = new JitterObject("jit.gl.gridshape","ListenWindow");
mysphere[i].shape = "sphere";
mysphere[i].lighting_enable = 1;
mysphere[i].smooth_shading = 1;
mysphere[i].scale = [0.05,0.05,0.05];
mysphere[i].color = [Math.random(),Math.random(),Math.random(),0.5] ;
mysphere[i].position = [Math.random()*2.-1, Math.random()*2.-1];
mysphere[i].blend_enable = 1;
}
// create a JitterListener for our [jit.window] object
var mylistener = new JitterListener(mywindow.getregisteredname(), thecallback);
function randomize() // reorient the little spheres
{
for(var i="0;i<32;i++)" {
mysphere[i].color = [Math.random(),Math.random(),Math.random(),0.5] ;
mysphere[i].position = [Math.random()*2.-1, Math.random()*2.-1];
}
}
function thecallback(event)
// callback function to handle events triggered by mousing
// in our [jit.window]
{
var x,y,button; // some local variables
if (event.eventname="="mouse")" {
// we're entering, dragging within, or leaving a "mouse click" event
// arguments are (x,y,button,cmd,shift,capslock,option,ctrl)...
// we only care about the first three
x = event.args[0];
y = event.args[1];
button = event.args[2];
if (button) // we're clicked down
{
mywidget.color = [0,1,0,1]; // color our control object green
mywidget.shape = "torus"; // change it to a donut shape
}
else // we've just unclicked
{
mywidget.color = [1,0,0,1]; // color our object red
mywidget.shape = "sphere"; // change back to a sphere
}
}
else if (event.eventname="="mouseidle")" {
// we're mousing over the window with the mouse up
x = event.args[0];
y = event.args[1];
mywidget.color = [1,1,1,1] ; // color our object white
}
else if (event.eventname="="mouseidleout")" {
// we're no longer mousing over the window
x = event.args[0];
y = event.args[1];
mywidget.color = [1,1,1,0.5] ; // make our object translucent
}
// move our control object to the drawing context's
// equivalent of where our mouse event occurred:
mywidget.position = myrender.screentoworld(x,y);
}
// don't allow this function to be called from Max
thecallback.local = 1;
function bang()
// main drawing loop... figure out which little spheres to move
// and drive the renderer
{
// collision detection block. we need to iterate through
// the little spheres and check their distance from the control
// object. if we're touching we move the little sphere away
// along the correct angle of contact.
for(var i = 0;i<OBJECT_COUNT;i++) {
// cartesian distance along the x and y axis
var distx = mywidget.position[0]-mysphere[i].position[0];
var disty = mywidget.position[1]-mysphere[i].position[1];
// polar distance between the two objects
var r = Math.sqrt(distx*distx+disty*disty);
// angle of little sphere around control object
var theta = Math.atan2(disty,distx);
// check for collision...
if(r<0.15)
// control object is size 0.1, little spheres are 0.05,
// so less than 0.15 and it's a hit...
{
// convert polar->cartesian to figure out x and y displacement
var movex = (0.15-r)*Math.cos(theta);
var movey = (0.15-r)*Math.sin(theta);
// offset the little sphere to the new position,
// which should be just beyond touching at the
// angle of contact we had before. the result
// should look like we've "pushed" it along...
mysphere[i].position = [mysphere[i].position[0]-movex, mysphere[i].position[1]-movey];
}
}
// rendering block...
myrender.erase(); // erase the drawing context
myrender.drawclients(); // draw the client objects
myrender.swap(); // swap in the new drawing
}
function fullscreen(v)
// function to send the [jit.window] into fullscreen mode
{
mywindow.fullscreen = v;
}