Download Series Assets

Tutorial 47: Using Jitter Object Callbacks in JavaScript

This tutorial references the patcher 47jJitterListener.maxpat

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.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. For example, when you read a movie file into jit.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.

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.
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.
Pushing the spheres around.

Creating OpenGL objects in JavaScript

  • 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 callback function

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).
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 for the callback function

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 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
   }
   // move our control object to the drawing context's
   // equivalent of where our mouse event occurred:
   mywidget.position = myrender.screentoworld(x,y);
}

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.

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.

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
   mywidget.position = myrender.screentoworld(x,y);
}

This code executes whenever we drag our pointer across the jit.window object without clicking the pointer. First we store the variables x and y based on the pointer position, we turn our jit.gl.gridshape object (mywidget) white, in case the previous callback had turned it a different color, and again we reposition mywidget.

else if (event.eventname="="mouseidleout")" {
   // we're no longer mousing over the window
   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". All we do is change the alpha to 0.5, leaving the sphere where it was.

}
// 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.

Drawing the scene

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.

Summary

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.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.

Code Listing

// 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 for thecallback
  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
   }
   // move our control object to the drawing context's
   // equivalent of where our mouse event occurred:
   mywidget.position = myrender.screentoworld(x,y);
  }
  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
   // move our control object to the drawing context's
   // equivalent of where our mouse event occurred:
   mywidget.position = myrender.screentoworld(x,y);
  }
  else if (event.eventname="="mouseidleout")" {
  // we're no longer mousing over the window
   mywidget.color = [1,1,1,0.5] ; // make our object translucent
  }


}
// 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;
}

See Also