PhET-iO Developer's Guide
After you are familiar with the PhET-iO features and demos at https://phet-io.colorado.edu/, and after you have obtained a license for PhET-iO development, you can use this guide to start developing your own wrappers. A wrapper is an HTML page that embeds a PhET-iO simulation in an iframe and provides added value, such as simulation customization, data logging, dynamic feedback, classroom activities, etc. The wrapper communicates with the simulation iframe using a JavaScript API called SimIFrameClient.js
Getting started
Here is an example wrapper that shows how to connect to a PhET-iO from a wrapper, with hooks for initialization, customization, listening for data streams, etc.
<!--
Copyright 2016, University of Colorado Boulder
This PhET-iO file requires a license
USE WITHOUT A LICENSE AGREEMENT IS STRICTLY PROHIBITED.
For licensing, please contact phethelp@colorado.edu
Minimal template for developing a wrapper. Used as a template for devguide.
@author Sam Reid (PhET Interactive Simulations)
-->
<head>
<title>PhET-iO Wrapper Harness</title>
</head>
<body>
<!-- Specify the simulation to run in the iframe. -->
<iframe id="sim" width="768" height="464"></iframe>
<!--Load PhET-iO scripts.-->
<script type="text/javascript"
src="https://phet-io.colorado.edu/sims/faradays-law/1.3.2-phetio/lib/assert.js"></script>
<script type="text/javascript"
src="https://phet-io.colorado.edu/sims/faradays-law/1.3.2-phetio/lib/SimIFrameClient.js"></script>
<script type="text/javascript"
src="https://phet-io.colorado.edu/sims/faradays-law/1.3.2-phetio/lib/WrapperUtils.js"></script>
<script type="text/javascript"
src="https://phet-io.colorado.edu/sims/faradays-law/1.3.2-phetio/lib/QueryStringMachine.js"></script>
<script>
var sim = WrapperUtils.getSim( 'faradays-law', '1.3.2-phetio' );
// Construct the sim iframe client that can be used to send messages to the sim.
var simIFrameClient = new SimIFrameClient( document.getElementById( 'sim' ) );
// Choose details of how the sim is launched and fill in callbacks for events
simIFrameClient.launchSim( sim.URL, {
// Choose whether the sim should emit states and/or input events in addition to the instance-based messages
// Can be overriden by query parameters in the wrapper phet-io.emitStates and phet-io.emitInputEvents
emitStates: false,
emitInputEvents: false,
// Callback for messages from the sim's event stream
phetioEventsListener: function( message ) {
console.log( 'message emitted: ', JSON.parse( message ) );
},
// Add customization that will be applied as the simulation launches
expressions: [], // Takes the form { phetioID: {string}, method: {string}, args: {Object[]} }.
// Callback when the sim is initialized
onSimInitialized: function() {
console.log( 'sim initialized' );
// Send other commands to simIFrameClient as desired, using simIFrameClient.invoke or invokeSequence
}
} );
</script>
</body>
Note that this example code is hard-coded to use "faradays-law" version "1.3.2-phetio". When you begin development of your own wrapper, you'll need to replace those strings with the strings appropriate for your simulation and version.
A good place to start is by downloading the example above and launching it on your development machine. Open the console to see the messages that are printed to the console. Once that is working, you can start changing the commands sent across the simIFrameClient and switch to a different sim/version. The links for the SimIFrameClient, WrapperUtils and simulation itself should be taken from the same version of the simulation, this will ensure they are using a compatible communication protocol.
To send one command to one simulation instance, use simIFrameClient.invoke() as shown in the example above. To invoke multiple commands, use simIFrameClient.invokeSequence(), passing in an argument like startupCustomization in the example above.
Instrumented Instances
Each instrumented instance in a simulation is associated with an identifier (called a phetioID) and a type. The phetioID:
- appears in messages produced by the object in the data log
- is used to configure the object and
- is used to interact with the object dynamically
The type describes the nature of the object as well as methods for interacting with the object. The full identifier for an object is given by a dot-delimited string. For example, the in Faraday's Law, the magnet's position has the phetioID:
'faradaysLaw.faradaysLawScreen.magnet.position'
Note that all identifiers are prefixed by the name of the simulation and the name of the screen within the simulation to facilitate cross-simulation studies. To identify the names and types of all of the instrumented instances in a simulation, launch the "Instance Proxies" wrapper. To see what methods are available for a type, please refer to the "Types" section below.
Query Parameters
Several query parameters govern the behavior of an instrumented PhET simulation:
- phet-io.emitStates: Outputs the full state at the end of every frame.
- phet-io.emitDeltas: Outputs state keyframes every 10 seconds and deltas every frame
- phet-io.emitEmptyDeltas: When emitting states using phetio.js, emit deltas that are empty, to simplify playback in some systems.
- phet-io.log: If set to console, will stream phetioEvents to console. This is useful for understanding or debugging the phetioEvent log output in real-time. If using Chrome or Firefox, using phet-io.log=lines provides a colorized single-line output.
- phet-io.expressions: Evaluate expressions on phetio wrapper objects, like:
Described in more detail below.http://localhost/faradays-law/faradays-law_en.html?ea&brand=phet-io&phet-io.log=console&phet-io.expressions=[["beaker.beakerScreen.soluteSelector","setVisible",[true]]]
Example Wrappers
Additionally, when you obtain a license for PhET-iO development, you will receive a password to access the protected wrappers for each PhET-iO simulation. The wrappers demonstrate the PhET-iO functionality of the simulation, provide information for developing wrappers and provide a starting point for development.
Data Stream
Here is (the beginning of) a sample data log from the Color Vision simulation:
{
"messageIndex": 0,
"eventType": "model",
"phetioID": "sim",
"componentType": "TSim",
"event": "simStarted",
"time": 1447285103220,
"parameters": {
"sessionID": null,
"simName": "Color Vision",
"simVersion": "1.1.0-phet-io.3",
"url": "http://www.colorado.edu/physics/phet/dev/html/phet-io/newschools/color-vision_en.html",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/46.0.2490.80 Safari/537.36",
"provider": "PhET Interactive Simulations, University of Colorado Boulder"
}
}
{
"messageIndex": 1,
"eventType": "user",
"phetioID": "homeScreen.singleBulbScreenLargeButton",
"componentType": "TPushButton",
"event": "fired",
"time": 1447285111392,
"children": [
{
"messageIndex": 2,
"eventType": "model",
"phetioID": "sim.showHomeScreen",
"componentType": "TProperty",
"event": "changed",
"time": 1447285111393,
"parameters": {
"oldValue": true,
"newValue": false
}
}
]
}
{
"messageIndex": 3,
"eventType": "user",
"phetioID": "colorVision.singleBulbScreen.photonRadioButton",
"componentType": "TRadioButton",
"event": "fired",
"time": 1447285114104,
"parameters": {
"value": "photon"
},
"children": [
{
"messageIndex": 4,
"eventType": "model",
"phetioID": "colorVision.singleBulbScreen.beamType",
"componentType": "TProperty",
"event": "changed",
"time": 1447285114104,
"parameters": {
"oldValue": "beam",
"newValue": "photon"
}
}
]
}
Here is a brief description of each of the fields in a message:
Attribute | Description |
---|---|
messageIndex | The number of the message, starting with 0. Can be used to verify that the correct number of messages was received. |
eventType | {user|model} whether the message was originated by a user action or by the simulation itself. For instance, a button press is a user event while a state change in the simulation is a model event (even if it was triggered by a user event) |
phetioID | The unique identifier for the object sending the message. It usually starts with ${simulation-name}.${screen-name} except for globals outside of screens, or objects in the navigation bar. |
componentType | The name of the component type of the object that sent the message. Component type names begin with a capital letter T because they refer to Type wrappers. |
event | The name of the event that was fired by the object. |
time | The number of milliseconds since the epoch (in 1960) as reported by the client machine. No effort is made to verify that this matches any other clock. |
parameters | (optional) Key value pairs that describe the details of the event. |
children | (optional) Events that were triggered while this event was being processed. |
The events are delivered as JSON objects, and must be parsed using a JSON parser. JSON and JavaScript do not guarantee the order of the fields, so the data stream cannot be parsed as simple text under the assumption that fields appear in a deterministic ordering.
iframe API
The iframe API can be used to communicate with a running simulation. Methods can be called on any of the simulation instances. Commands can be sent to them, or values retrieved from them. Here's a full example of how to use the sim iframe API to toggle whether a simulation is active (running) or inactive (frozen), which also demonstrates how to send commands to the simulation and receive messages back from the simulation.
// Get a reference to the iframe for the sim
var simFrame = document.getElementById( 'sim' );
// This sim does not need any listeners to be added before the sim launches,
// so it can be phet-io.standalone
simFrame.src = sim.URL + '&phet-io.standalone';
// Construct the sim iframe client that can be used to send messages to the sim.
var simIFrameClient = new SimIFrameClient( simFrame );
// Keep track of whether the sim is active or not
var lastActiveValue = true;
// When the user presses the "Toggle Active" button, send a command to the iframe
document.getElementById( 'toggleActiveButton' ).addEventListener( 'click', function() {
// Send a message using the simIFrameClient to invoke a method on an instance
simIFrameClient.invoke( sim.camelCaseName + '.sim.active', 'setValue', [ !lastActiveValue ] );
} );
// When the sim launches, wire up a listener to the active Property,
// so we can report its value and use the correct value when toggling
simIFrameClient.onSimInitialized( function() {
// Send a message using the simIFrameClient to link up to the active Property
simIFrameClient.invoke( sim.camelCaseName + '.sim.active', 'link', [ function( active ) {
lastActiveValue = active;
document.getElementById( 'readout' ).innerHTML = 'Active: ' + active;
} ] );
} );
There are two ways to pass a function across the API, one which runs the function in the parent frame and one which runs the function in the sim frame. For instance, in the above example the args has a function wrapped in an array. This will run in the parent frame. If passing an object literal with {phetioID, method, args}, it will be invoked synchronously in the simulation frame, see the source for state.html for an example.
Startup Sequence
In order to prevent missing any events sent from the simulation to the wrapper (in the case of multi-threaded browser support for multiple frames), PhET-iO provides a startup sequence with a step where the wrapper can register any listeners with phet-io, before the simulation launches. This pattern is implemented in the example provided above. Here is an overview of the startup sequence:
- the wrapper starts up and initializes its state
- the wrapper creates a listener with `window.addEventListener( 'message',...)`
- the wrapper sets the source of the simulation iframe
- the simulation preload files initialize (including phetio.js and related files)
- the simulation creates a listener with `window.addEventListener( 'message',...)`
- the simulation does postMessage indicating that it is ready for phet-io messages
- the wrapper receives the postMessage and sends a composite command to the simulation frame, containing adding any desired listeners, setting any desired startup configuration, etc and ending with the directive to start launching the requirejs portion of the simulation. Alternatively, this could be a sequence of individual messages, but the wrapper would have to wait for the response message for each before sending the final go-ahead to launch the remainder of the simulation.
- the simulation receives the message to launch the requirejs portion and finishes launching normally.
// Wait for the phase where we can add listeners
simIFrameClient.onPhETiOInitialized( function() {
// Add any desired listeners to phet-io (cannot communicate with sim instances at this
// point since they have not been created yet
simIFrameClient.invoke( 'phetio', 'addPhETIOEventsListener', [ function( message ) {
textarea.innerHTML += '\n' + message;
textarea.scrollTop = textarea.scrollHeight;
} ], function() {
// after that is complete, launch the simulation
simIFrameClient.invoke( 'phetio', 'launchSimulation', [] );
}
);
} );
Types
Each instrumented instance in a PhET-iO simulation has a type which provides a set of methods which can be called from query parameters or across the iframe api. The events field declares which event names can be emitted by the type in the phetioEvents log. The "T" prefix in front of each type indicates that it is a thin "phet-io" wrapper type instead of the actual implementation simulation type. This section provides documentation for all instrumented types, including the events that can appear in the data stream and the methods associated with each type.
TAquaRadioButton
events: fired
- setCircleButtonVisible: (TBoolean) ==> TVoid
- Sets whether the circular part of the radio button will be displayed.
TArray
- setValue: () ==> TVoid
- Sets the value of all elements in the array
TBarrierRectangle
events: fired
TBoolean
TBounds2
TBounds3
TCheckBox
events: toggled
- isChecked: () ==> TBoolean
- Returns true if the checkbox is checked, false otherwise
- link: (TFunctionWrapper) ==> TVoid
- Link a listener to the underlying checked TProperty. The listener receives an immediate callback with the current value (true/false)
- setChecked: (TBoolean) ==> TVoid
- Sets whether the checkbox is checked or not
TColor
TComboBox
events: fired,popupShown,popupHidden
TDerivedProperty
events: changed
- getValue: () ==> TObject
- Gets the current value
- link: (TFunctionWrapper) ==> TVoid
- Adds a listener which will receive notifications when the value changes and an immediate callback with the current value upon linking.
- unlink: (TFunctionWrapper) ==> TVoid
- Removes a listener that was added with link
TEvents
events: documentation,fromStateObject,typeName,methods,supertype,getMethodDeclaration,allMethods
- addListener: (TString,TFunctionWrapper) ==> TVoid
- Adds a listener to the specified event channel
- removeListener: (TString,TFunctionWrapper) ==> TVoid
- Removes a listener that was added with addListener
TFaucet
events: startTapToDispense,endTapToDispense
TFunctionWrapper
THSlider
- setMajorTicksVisible: (TBoolean) ==> TVoid
- Set whether the major tick marks should be shown
- setMinorTicksVisible: (TBoolean) ==> TVoid
- Set whether the minor tick marks should be shown
THSliderTrack
TMeasuringTape
TMenuItem
events: fired
TMomentaryButton
events: pressed,released,releasedDisabled
TNode
- addPickableListener: (TFunctionWrapper) ==> TVoid
- Adds a listener for when pickability of the node changes
- isPickable: () ==> TBoolean
- Gets whether the node is pickable (and hence interactive)
- isVisible: () ==> TBoolean
- Gets a Boolean value indicating whether the node can be seen and interacted with
- setOpacity: (TNumber) ==> TVoid
- Set opacity between 0-1 (inclusive)
- setPickable: (TBoolean) ==> TVoid
- Set whether the node will be pickable (and hence interactive)
- setRotation: (TNumber) ==> TVoid
- Set the rotation of the node, in radians
- setVisible: (TBoolean) ==> TVoid
- Set whether the node will be visible (and interactive)
TNumber
TNumberControl
TObject
TObservableArray
events: itemAdded,itemRemoved
TOnOffSwitch
events: toggled
TPanel
TParticle
TPhETIO
events: simStarted,state,stateDelta,frameCompleted,stepSimulation,inputEvent,displaySize
- addExpressions: (TArray) ==> TVoid
- Set expressions to take effect before the simulation is launched, similar to phet-io.expressions, see TObject
- addInstanceAddedListener: (TFunctionWrapper) ==> TVoid
- Adds a listener that receives a callback whenever a new sim instance has been prepared for interoperability
- addInstanceRemovedListener: (TFunctionWrapper) ==> TVoid
- Removes a listener that was added with addInstanceAddedListener
- addPhETIOEventsListener: (TFunctionWrapper) ==> TVoid
- Adds a listener to the phetioEvents event channel, for data analysis
- endEvent: (TNumber) ==> TVoid
- End a message
- getAPI: () ==> TObject
- Get the simulation API as a JSON object
- getPhETIOIDs: () ==> TArray
- Gets a list of all of the wrapped instances which are available for interoperability.
- getRandomSeed: () ==> TNumber
- Get the random seed, used for replicable playbacks
- getState: () ==> TObject
- Gets the full state of the simulation
- getValues: (TArray) ==> TObject
- Get the current values for multiple Property/DerivedProperty at the same time. Useful for collecting data to be plotted, so values will be consistent.
- invokeInputEvent: (TString) ==> TVoid
- Plays back a recorded input event into the simulation.
- isInteractive: () ==> TBoolean
- Gets whether the sim can be interacted with (via mouse/touch).
- launchSimulation: () ==> TVoid
- Finish launching the simulation, called from a wrapper after all cross-frame initialization is complete
- rasterizeDisplay: (TFunctionWrapper) ==> TVoid
- Rasterize the display, asynchronously
- setDisplaySize: (TNumber,TNumber) ==> TVoid
- set the size of the visible region for the simulation
- setInteractive: (TBoolean) ==> TVoid
- Sets whether the sim can be interacted with (via mouse/touch).
- setPlaybackMode: (TBoolean) ==> TVoid
- Disable the sim clock driver so that time will only pass as it is played back from a log file.
- setRandomSeed: (TNumber) ==> TVoid
- Set the random seed, used for replicable playbacks
- setState: (TObject) ==> TVoid
- Sets the full state of the simulation
- startEvent: (TObject) ==> TNumber
- Begin a message
- stepSimulation: (TNumber) ==> TVoid
- Steps one frame of the simulation.
- triggerEvent: (TObject) ==> TVoid
- Start and end a message
TPhetButton
events: fired
TPhetMenu
TProperty
events: changed
- getValue: () ==> TObject
- Gets the current value.
- link: (TFunctionWrapper) ==> TVoid
- Add a listener which will be called when the value changes. The listener also gets an immediate callback with the current value.
- setValue: (TObject) ==> TVoid
- Sets the value of the property, and triggers notifications if the value is different
- unlink: (TFunctionWrapper) ==> TVoid
- Removes a listener
TPushButton
events: fired
- fire: () ==> TVoid
- Performs the action associated with the button
TRadioButton
events: fired
TRandom
TResetAllButton
events: fired
TScreenButton
events: fired
- fire: () ==> TVoid
- Fire the button's action, as if the button has been pressed and released
TSim
events: simStarted
- addEventListener: (TString,TFunctionWrapper) ==> TVoid
- Add an event listener to the sim instance
- disableRequestAnimationFrame: () ==> TVoid
- Prevents the simulation from animating/updating
- getScreenshotDataURL: () ==> TString
- Gets a base64 representation of a screenshot of the simulation as a data url
TString
TTandem
TTandemButtonListener
events: up,over,down,out,fire
TTandemDragHandler
events: dragStarted,dragged,dragEnded
TTandemEmitter
events: emitted
TTandemText
events: textChanged
- getText: () ==> TString
- Get the text
- setText: (TString) ==> TVoid
- Set the text
TToggleButton
events: toggled
TVector2
TVector3
TVerticalCheckBoxGroup
TVoid
TWavelengthSlider
Tips and Tricks
Reset Issues
When the user presses the Reset All button in the simulation, it may overwrite some of the customization that has been previously applied (such as making a Node visible). Two workarounds for this problem are:
- Remove the Reset All button
- Add a listener to the Reset All button