Currently, this is written as if the user has experience with other scene graphs. Will include more information later.

Hello World

Include the Scenery script in the page after jQuery and LoDash, and create a block-level element (like a div) that has a width and height that won't change with its contents:

<div id="hello-world-scene" style="width: 400px; height: 100px;"></div>
      

The following JS code will initialize a scene graph, add some centered text into the scene, and paint it into the div.

// Create a scene graph over the block-level element. Everything inside is replaced
var scene = new scenery.Scene( $( '#hello-world-scene' ) );

// Add our text
scene.addChild( new scenery.Text( 'Hello World', {
  centerX: 200, // the center of our text's bounds is at x = 200
  centerY: 50, // the center of our text's bounds is at y = 50
  font: '25px sans-serif' // any CSS 'font' declaration will work
} ) );

// Paint any changes (in this case, our text).
scene.updateScene();
      

There is a standalone example of this Hello World demo.

Shapes

Nodes are the basic structure of the scene graph, and can be given shapes. Shapes can be built from convenience functions, or from Canvas-style calls. Exact bounding boxes of shapes are calculated, including any strokes.

// A rectangle at (10,10) with a width of 50 and height of 80
scene.addChild( new scenery.Path( kite.Shape.rectangle( 10, 10, 50, 80 ), {
  fill: '#ff0000'
} ) );

// An octogon, with a radius of 30
scene.addChild( new scenery.Path( kite.Shape.regularPolygon( 8, 30 ), {
  fill: '#00ff00',
  stroke: '#000000',
  x: 100, // the shape is centered at the origin, so offset it by this x and y
  y: 50
} ) );

// Custom shapes can also be created
var customShape = new kite.Shape();
customShape.moveTo( 0, 0 );
customShape.lineTo( 40, 40 );
customShape.quadraticCurveTo( 80, 0, 40, -40 );
customShape.lineTo( 40, 0 );
customShape.close();
// NOTE: this can also be chained, like shape.moveTo( ... ).lineTo( ... ).close()

scene.addChild( new scenery.Path( customShape, {
  fill: '#0000ff',
  stroke: '#000000',
  lineWidth: 3,
  x: 150,
  y: 50
} ) );
      

Nodes

Nodes can be created and modified currently in a few different ways:

// The following are all equivalent ways to create a node, listed in approximate order of increasing performance (if it is relevant).

var someShape = ...; // a shape used for brevity in the following calls, otherwise setting the bottom of the node makes no sense

// Declarative pattern. NOTE: the parameters are executed in a specific order which is particularly important
// for transformation and bounds-based parameters. Using translations like 'x' or 'bottom' do not change the
// point around which rotation or scaling is done in the local coordinate frame, and bounds-based modifiers
// like 'bottom' (which moves the bottom of the node's bounds to the specific y-value) are handled after other
// modifiers have been run.
var node1 = new scenery.Path( someShape, {
  fill: '#000000',
  rotation: Math.PI / 2,
  x: 10,
  bottom: 200
} );

// ES5-setter style, called internally by the declarative paramter object style above
var node2 = new scenery.Path();
node2.shape = someShape;
node2.fill = '#000000';
node2.x = 10; // note the reordering, as this is precisely how the declarative style is executed
node2.rotation = Math.PI / 2;
node2.bottom = 200;

// Java-style setters, called internally from the ES5-setter style above
var node3 = new scenery.Path();
node3.setShape( someShape );
node3.setFill( '#000000' );
node3.setX( 10 );
node3.setRotation( Math.PI / 2 );
node3.setBottom( 200 );

// Chained style, since setters return a this-reference.
var node4 = new scenery.Path().setShape( someShape ).setFill( '#000000' ).setX( 10 ).setRotation( Math.PI / 2 ).setBottom( 200 );

// Additionally, parameters can be accessed by two styles:
node2.rotation === node3.getRotation();
      

Similarly to Piccolo2d, nodes have their 'self' paint/bounds and the paint/bounds of their children, where the node's self is rendered first (below), and then children are painted one-by-one on top. Additionally, it borrows Piccolo's coordinate frames where there are local, parent, and global coordinate frames. The example below will hopefully be illustrative.

// create a node that will have child nodes.
var container = new scenery.Path( kite.Shape.rectangle( 0, 0, 300, 100 ), {
  // we'll add a background shape (300x100 rectangle) that will render on this node (it will be rendered below any child nodes)
  fill: '#888888',
  x: 20, // this node and all of its children will be offset by (20,20)
  y: 20
} );
scene.addChild( container );

// bounding box of the container (and its children so far) in its parent's coordinate system.
// translated by (20,20 due to the x,y parameters)
container.getBounds(); // x between [20, 320], y between [20, 120]

// bounding box of the container's own rendering/paint (the rectangle) in its local coordinate frame (without applying other transforms)
container.getSelfBounds(); // x between [0, 300], y between [0, 100]

// add a green rectangle to the container, which will be above the gray background
var child1 = new scenery.Path( kite.Shape.rectangle( 0, 0, 25, 25 ), { // a 25x25 rectangle
  fill: '#00ff00',
  scale: 2, // but scaled so it is effectively a 50x50 rectangle to its parents
  left: 10, // 10px to the right of our parent's left bound
  centerY: container.height // vertically center us on the container's bottom bound
} );
container.addChild( child1 );

// bounding box of the child in its parent's coordinate frame (the container's local coordinate frame)
// note that since it is scaled 2x, its dimensions appear to be 50x50
child1.getBounds(); // x between [10, 60], y between [75, 125], (with a left of 10 and centered vertically on 100)

child1.getSelfBounds(); // x between [0, 25], y between [0, 25] -- just bounds of its shape

// but now that we have added a child, getBounds() on the container (which contains the bounds of its children) has changed:
container.getBounds(); // x between [20, 320], y between [20, 145] -- bottom bound of 145 since it has an x offset of 20 plus its child's bottom of 125

// and some text to the same container, which will be on top of both the background and green rectangle
var child2 = new scenery.Text( 'On Top?', {
  left: child1.centerX,
  centerY: child1.centerY,
  font: '20px sans-serif'
} );
container.addChild( child2 );
      

Images

For now, pass in a valid image and it will be rendered with its upper-left corner at 0,0 in the local coordinate frame.

// TODO: support different ways of handling the asynchronous load
var thumbnailImage = document.createElement( 'img' );
thumbnailImage.onload = function( e ) {
  scene.addChild( new scenery.Image( thumbnailImage ) );
  scene.addChild( new scenery.Image( thumbnailImage, {
    x: 200,
    y: 25,
    scale: 1.5,
    rotation: Math.PI / 4
  } ) );
  scene.updateScene();
};
thumbnailImage.src = 'http://phet.colorado.edu/sims/energy-skate-park/energy-skate-park-basics-thumbnail.png';
      

DOM Elements

DOM elements can be added in (they are transformed with CSS transforms). Currently, bounds-based modifiers may be buggy.

var element = document.createElement( 'form' );
element.innerHTML = '';

scene.addChild( new scenery.DOM( element, {
  x: 50,
  rotation: Math.PI / 4
} ) );
      

When added in the above manner, the DOM element will be lifted in front of any other non-lifted elements by default. It is possible to have the DOM elements reside where they would normally be rendered.

Animation

It is recommended to use scene.updateScene() inside of requestAnimationFrame(), since updateScene() attempts to only re-paint content that is inside of changed areas. This type of pattern may be refactored out soon.

var div = $( '#animation-simple' );
var scene = new scenery.Scene( div );

// a hexagon to rotate
var node = new scenery.Path( kite.Shape.regularPolygon( 6, 90 ), {
  fill: '#000000',
  centerX: 100,
  centerY: 100
} );
// a marker so pauses on the iPad animation (since requestAnimationFrame doesn't trigger while scrolling) are visible
node.addChild( new scenery.Path( kite.Shape.rectangle( 0, -3, 60, 6 ), {
  fill: '#ff0000'
} ) );
scene.addChild( node );

// keep track of how much time elapsed over the last frame
var lastTime = 0;
var timeElapsed = 0;
function tick() {
  window.requestAnimationFrame( tick, div[0] );
  
  var timeNow = new Date().getTime();
  if ( lastTime != 0 ) {
    timeElapsed = (timeNow - lastTime) / 1000.0;
  }
  lastTime = timeNow;
  
  node.rotate( timeElapsed );
  scene.updateScene(); // repaints dirty regions. use renderScene() to render everything
}
window.requestAnimationFrame( tick, div[0] );
      

Input Events

The input event system is somewhat different from many other scene graphs, since it hopes to accomodate multi-touch interaction with the mouse. It comes with a low-level event system of which then gestures and behavior can be built on top. For instance, what if in some cases you want a zoom-pinch to be interrupted by another pointer manipulating a control? When are pointers interacting with elements individually (sliders, dragging objects, etc.), when are they acting together (zooming, rotating, etc.), and how does this behavior change?

An instance of scenery.Input is created and hooked to an event source with functions like Scene.initializeFullscreenEvents() and Scene.initializeStandaloneEvents(). The Input object tracks the principal abstraction of a pointer. A pointer represents either the mouse or a single touch as it is tracked across the screen. The mouse 'pointer' always exists, but touch pointers are transient, created when an actual pointer is pressed on a touchscreen and detached when it is lifted (or canceled). Input event listeners can be added to nodes, the scene, or actual pointers themselves. Attaching a listener to a pointer allows tracking of that pointer's state, and when combined with behavioral flags can create advanced input handling systems. Also to note: preventDefault (triggered on events by default) will prevent touch events from being re-fired as mouse events, so we only see them once.

Events can be handled by any input listener (return true from the callback) and it will not fire any subsequent input listeners for that event. For a single touchmove event, individual pointers generate their own events, so each input event will have a single associated pointer. The order of listeners visited for each event is as follows:

  1. Listeners attached to the associated pointer
  2. For move/up/down/cancel events, listeners for the node directly beneath the event, and subsequently for all parent (ancestor) nodes in order up to the root node
  3. For enter/exit events, listeners similar to move/up/down/cancel events, but only up to (and not including) the common parent before/after the action. TODO: better explanation
  4. Listeners attached to the scene

DOM interaction with the event system is still being worked on. Gesture listeners and full pointer-list access will be added.

Below are a series of examples that will hopefully show off the current system.

Colored by quantity of pointers inside shape

Pending decision of whether enter/exit occurs for touchstart or touchend. Seems currently to not handle drags from outside of the bounds! (TODO: investigate). Also not including mouseout or mouseover yet (TODO: decide on how this should work) Try to get 10 pointers in the hexagon on the iPad!

Pointers in hexagon: 0

$( window ).ready( function() {
  var $main = $( '#input-mouseover' );
  var scene = new scenery.Scene( $main );
  
  // hook up event listeners just on this scene (not the whole document)
  scene.initializeStandaloneEvents();
  
  var count = 0;
  var colors = [ '000000', 'ff0000', '00ff00', '0000ff', 'ffff00', '00ffff', 'ff00ff', 'ffffff' ];
  var maxColor = colors.length - 1;
  
  // TODO: get faster text bounds detection. it's too slow right now on the iPad
  var labelPrefix = 'Pointers in hexagon: ';
  // var label = new scenery.Text( labelPrefix + count, {
  //   left: 20,
  //   top: 20,
  //   font: '20px sans-serif'
  // } );
  // scene.addChild( label );
  
  // a big hexagon in the center. stroke not included in the hit region by default
  var node = new scenery.Path( kite.Shape.regularPolygon( 6, 150 ), {
    fill: colors[count],
    stroke: '#000000',
    centerX: 200,
    centerY: 200
  } );
  scene.addChild( node );
  
  // update our label and color
  function updatePointers() {
    node.fill = colors[ Math.min( count, maxColor ) ];
    // label.setText( labelPrefix + count );
    $( '#hex-pointer-count' ).text( labelPrefix + count );
  }
  
  // listener fired whenever the event occurs over the node
  // below are the main 6 input events that are fired
  node.addInputListener( {
    // mousedown or touchstart (pointer pressed down over the node)
    down: function( event ) {
      if ( !event.pointer.isMouse ) {
        count++;
        updatePointers();
      }
    },

    // mouseup or touchend (pointer lifted from over the node)
    up: function( event ) {
      if ( !event.pointer.isMouse ) {
        count--;
        updatePointers();
      }
    },
    
    // triggered from mousemove or touchmove (pointer moved over the node from outside)
    enter: function( event ) {
      count++;
      updatePointers();
    },
    
    // triggered from mousemove or touchmove (pointer moved outside the node from inside)
    exit: function( event ) {
      count--;
      updatePointers();
    },
    
    // platform-specific trigger.
    // on iPad Safari, cancel can by triggered by putting 4 pointers down and then dragging with all 4
    cancel: function( event ) {
      count--;
      updatePointers();
    },
    
    // mousemove (fired AFTER enter/exit events if applicable)
    move: function( event ) {
      // do nothing
    }
  } );
  
  // repaint loop.
  function tick() {
    window.requestAnimationFrame( tick, $main[0] );
    scene.updateScene();
  }
  window.requestAnimationFrame( tick, $main[0] );
} );
      

Node Dragging with Swipe-across-to-start-drag

Visit this standalone example, and you can drag multiple hexagons with multiple pointers, and sliding a pointer across a touch-screen (i.e not the mouse) will pick up the first hexagon it slides across, into a drag.

Appendix (to be completed later)

An example of math with MathJax, verifying it doesn't trample dollar signs in pre tags: $f(\theta)^2$, so we'll able to include the necessary discussions using matrix algebra, etc.