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 Node as the root of our tree (it can contain children)
const scene = new phet.scenery.Node();

// Create a display using this root node, using our existing div
const display = new phet.scenery.Display( scene, {
  container: document.getElementById( 'hello-world-scene' )
} );

// Add our text
scene.addChild( new phet.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).
display.updateDisplay();
      

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 phet.scenery.Path( phet.kite.Shape.rectangle( 10, 10, 50, 80 ),{
  fill: '#ff0000'
} ) );

// An octogon, with a radius of 30
scene.addChild( new phet.scenery.Path( phet.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
const customShape = new phet.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 phet.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).

const 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.
const node1 = new phet.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
const node2 = new phet.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
const node3 = new phet.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.
const node4 = new phet.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.
const container = new phet.scenery.Path( phet.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
const child1 = new phet.scenery.Path( phet.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
const child2 = new phet.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
const thumbnailImage = document.createElement( 'img' );
thumbnailImage.onload = function( e ) {
  scene.addChild( new phet.scenery.Image( thumbnailImage ) );
  scene.addChild( new phet.scenery.Image( thumbnailImage, {
    x: 200,
    y: 25,
    scale: 1.5,
    rotation: Math.PI / 4
  } ) );
  display.updateDisplay();
};
thumbnailImage.src = 'https://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.

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

scene.addChild( new phet.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 display.updateDisplay() inside of requestAnimationFrame(), since updateScene() attempts to only re-paint content that is inside of changed areas. For simple usage, you can use updateOnRequestAnimationFrame() on the display.

const scene = new phet.scenery.Node();
const display = new phet.scenery.Display( scene, {
  container: document.getElementById( 'animation-simple' )
} );

// a hexagon to rotate
const node = new phet.scenery.Path( phet.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 phet.scenery.Path( phet.kite.Shape.rectangle( 0, -3, 60, 6 ), {
  fill: '#ff0000'
} ) );
scene.addChild( node );

// given time elapsed in seconds
display.updateOnRequestAnimationFrame( function( timeElapsed ) {
  node.rotate( timeElapsed );
} );
      

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 phet.scenery.Input is created and hooked to an event source with functions like Scene.initializeEvents(). 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

const scene = new phet.scenery.Node();
const display = new phet.scenery.Display( scene, {
  container: document.getElementById( 'input-mouseover' )
} );

// hook up event listeners just on this scene (not the whole document)
display.initializeEvents();

const count = 0;
const colors = [ '000000', 'ff0000', '00ff00', '0000ff', 'ffff00', '00ffff', 'ff00ff', 'ffffff' ];
const maxColor = colors.length - 1;

const labelPrefix = 'Pointers in hexagon: ';
const label = new phet.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
const node = new phet.scenery.Path( phet.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.setString( 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 instanceof phet.scenery.Mouse ) ) {
      count++;
      updatePointers();
    }
  },

  // mouseup or touchend (pointer lifted from over the node)
  up: function( event ) {
    if ( !( event.pointer instanceof phet.scenery.Mouse ) ) {
      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 without doing anything extra per-frame
display.updateOnRequestAnimationFrame();
      

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.