Currently, this is written as if the user has experience with other scene graphs. Will include more information later.
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.
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 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 );
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 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, allowInput: true } ) );
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.
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 ); } );
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:
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.
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();
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.
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.