示例#1
0
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var RectangularPushButton = require( 'SUN/buttons/RectangularPushButton' );
  var StopSignNode = require( 'SCENERY_PHET/StopSignNode' );

  /**
   * @constructor
   *
   * @param {Object} [options]
   */
  function StopButton( options ) {
    RectangularPushButton.call( this, _.extend( {
      xMargin: 7,
      yMargin: 3,
      touchAreaXDilation: 6,
      touchAreaYDilation: 6,
      baseColor: 'rgb( 231, 232, 233 )',
      content: new StopSignNode( {
        scale: 0.4
      } )
    }, options ) );
  }

  pendulumLab.register( 'StopButton', StopButton );

  return inherit( RectangularPushButton, StopButton );
} );
示例#2
0
define( function( require ) {
  'use strict';

  // modules
  var EnergyBox = require( 'PENDULUM_LAB/energy/view/EnergyBox' );
  var inherit = require( 'PHET_CORE/inherit' );
  var NumberProperty = require( 'AXON/NumberProperty' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' );
  var PendulumLabScreenView = require( 'PENDULUM_LAB/common/view/PendulumLabScreenView' );

  /**
   * @constructor
   *
   * @param {PendulumLabModel} model
   */
  function EnergyScreenView( model, options ) {

    PendulumLabScreenView.call( this, model, options );

    // @protected {Property.<number>}
    this.chartHeightProperty = new NumberProperty( 200 );

    // create and add energy graph node to the bottom layer
    var energyGraphNode = new EnergyBox( model, this.chartHeightProperty, {
      left: this.layoutBounds.left + PendulumLabConstants.PANEL_PADDING,
      top: this.layoutBounds.top + PendulumLabConstants.PANEL_PADDING
    } );
    this.energyGraphLayer.addChild( energyGraphNode );

    // @protected {EnergyBox}
    this.energyGraphNode = energyGraphNode;

    // move ruler and stopwatch to the right side
    this.rulerNode.centerX += ( energyGraphNode.width + 10 );
    model.ruler.setInitialLocationValue( this.rulerNode.center );

    this.stopwatchNode.left = this.rulerNode.right + 10;
    model.stopwatch.setInitialLocationValue( this.stopwatchNode.center );

    this.resizeEnergyGraphToFit();
  }

  pendulumLab.register( 'EnergyScreenView', EnergyScreenView );

  return inherit( PendulumLabScreenView, EnergyScreenView, {
    /**
     * Changes the chart height so that the energy graph fits all available size
     * @protected
     */
    resizeEnergyGraphToFit: function() {
      var currentSpace = this.toolsControlPanelNode.top - this.energyGraphNode.bottom;
      var desiredSpace = PendulumLabConstants.PANEL_PADDING;

      this.chartHeightProperty.value += currentSpace - desiredSpace;
    }
  } );
} );
示例#3
0
define( function( require ) {
  'use strict';

  // modules
  var BooleanProperty = require( 'AXON/BooleanProperty' );
  var inherit = require( 'PHET_CORE/inherit' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var Property = require( 'AXON/Property' );

  /**
   * @constructor
   *
   * @param {boolean} isInitiallyVisible
   */
  function MovableComponent( isInitiallyVisible ) {
    // @public {Property.<Vector2|null>} - Initial value will be set in view, after calculating all bounds of nodes
    this.locationProperty = new Property( null );

    // @public {Property.<boolean>} flag to determine stopwatch state
    this.isVisibleProperty = new BooleanProperty( isInitiallyVisible );
  }

  pendulumLab.register( 'MovableComponent', MovableComponent );

  return inherit( Object, MovableComponent, {
    /**
     * Function that sets the initial location of a movable object and keeps an internal copy of it.
     * @public
     *
     * @param {Vector2} initialLocation
     */
    setInitialLocationValue: function( initialLocation ) {

      // position to use for resetting
      // make a copy of the initial location vector
      this.initialLocation = initialLocation.copy();

      // set the location to the initial location
      this.locationProperty.value = this.initialLocation.copy();
    },

    /**
     * Reset function
     * @public
     */
    reset: function() {

      // Reset the location to the initial location
      this.locationProperty.value = this.initialLocation ? this.initialLocation.copy() : null;

      this.isVisibleProperty.reset();
    }
  } );
} );
示例#4
0
define( function( require ) {
  'use strict';

  // modules
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );

  // strings
  var customString = require( 'string!PENDULUM_LAB/custom' );
  var earthString = require( 'string!PENDULUM_LAB/earth' );
  var jupiterString = require( 'string!PENDULUM_LAB/jupiter' );
  var moonString = require( 'string!PENDULUM_LAB/moon' );
  var planetXString = require( 'string!PENDULUM_LAB/planetX' );

  /**
   * @constructor
   *
   * @param {string} title
   * @param {number|null} gravity - Gravitational acceleration on body (m/s^2) if defined.
   */
  function Body( title, gravity ) {
    // @public {string} (read-only)
    this.title = title;

    // @public {number|null} (read-only) - Gravitation acceleration (if available) in meters/second^2
    this.gravity = gravity;
  }

  pendulumLab.register( 'Body', Body );

  Body.MOON = new Body( moonString, 1.62 );
  Body.EARTH = new Body( earthString, 9.81 );
  Body.JUPITER = new Body( jupiterString, 24.79 );
  Body.PLANET_X = new Body( planetXString, 14.2 );
  Body.CUSTOM = new Body( customString, null );

  // array of all the bodies used in the simulation.
  Body.BODIES = [
    Body.MOON,
    Body.EARTH,
    Body.JUPITER,
    Body.PLANET_X,
    Body.CUSTOM
  ];

  // verify that enumeration is immutable, without the runtime penalty in production code
  if ( assert ) { Object.freeze( Body ); }

  return Body;
} );
示例#5
0
define( function( require ) {
  'use strict';

  // modules
  var Body = require( 'PENDULUM_LAB/common/model/Body' );
  var BooleanProperty = require( 'AXON/BooleanProperty' );
  var inherit = require( 'PHET_CORE/inherit' );
  var NumberProperty = require( 'AXON/NumberProperty' );
  var Pendulum = require( 'PENDULUM_LAB/common/model/Pendulum' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var Property = require( 'AXON/Property' );
  var RangeWithValue = require( 'DOT/RangeWithValue' );
  var Ruler = require( 'PENDULUM_LAB/common/model/Ruler' );
  var Stopwatch = require( 'PENDULUM_LAB/common/model/Stopwatch' );

  /**
   * @constructor
   *
   * @param {Object} [options]
   */
  function PendulumLabModel( options ) {
    var self = this;

    options = _.extend( {
      // {boolean} - Should be true if there is a PeriodTimer handling the trace's visibility.
      hasPeriodTimer: false,

      // {boolean}
      rulerInitiallyVisible: true
    }, options );

    // @public {Property.<Body>}
    this.bodyProperty = new Property( Body.EARTH );

    // @public {Property.<number>} - Gravitational acceleration
    this.gravityProperty = new NumberProperty( Body.EARTH.gravity );

    // @public {Property.<number>} - Tracked for the "Custom" body, so that we can revert to this when the user changes
    //                               from "Planet X" to "Custom"
    this.customGravityProperty = new NumberProperty( Body.EARTH.gravity );

    // @public {Property.<number>} - Speed of time.
    this.timeSpeedProperty = new NumberProperty( 1 );

    // @public {Property.<number>} - Number of visible pendula (2 pendula are handled in the model)
    this.numberOfPendulaProperty = new NumberProperty( 1 );

    // @public {Property.<boolean>}
    this.isPlayingProperty = new BooleanProperty( true );

    // @public {Property.<number>} - Friction coefficient
    this.frictionProperty = new NumberProperty( 0 );

    // @public {Property.<boolean}
    this.isPeriodTraceVisibleProperty = new BooleanProperty( false );

    // @public {Property.<number>}
    this.energyZoomProperty = new NumberProperty( 1 );

    // @public {Array.<Pendulum>}
    this.pendula = [
      new Pendulum( 0, 1, 0.7, true, this.gravityProperty, this.frictionProperty, this.isPeriodTraceVisibleProperty, options.hasPeriodTimer ),
      new Pendulum( 1, 0.5, 1.0, false, this.gravityProperty, this.frictionProperty, this.isPeriodTraceVisibleProperty, options.hasPeriodTimer )
    ];

    // @public (read-only) possible gravity range 0m/s^2 to 25m/s^2
    this.gravityRange = new RangeWithValue( 0, 25, this.gravityProperty.value );

    // @public (read-only) possible friction range
    this.frictionRange = new RangeWithValue( 0, 0.5115, 0 );

    // @public (read-only) model for ruler
    this.ruler = new Ruler( options.rulerInitiallyVisible );

    // @public (read-only) model for stopwatch
    this.stopwatch = new Stopwatch( false );

    // change gravity if body was changed
    this.bodyProperty.lazyLink( function( body, oldBody ) {
      // If it's not custom, set it to its value
      if ( body !== Body.CUSTOM ) {
        self.gravityProperty.value = body.gravity;
      }
      else {
        // If we are switching from Planet X to Custom, don't let them cheat (go back to last custom value)
        if ( oldBody === Body.PLANET_X ) {
          self.gravityProperty.value = self.customGravityProperty.value;
        }
        // For non-Planet X, update our internal custom gravity
        else {
          self.customGravityProperty.value = self.gravityProperty.value;
        }
      }
    } );

    // change body to custom if gravity was changed
    this.gravityProperty.lazyLink( function( gravity ) {
      if ( !_.some( Body.BODIES, function( body ) { return body.gravity === gravity; } ) ) {
        self.bodyProperty.value = Body.CUSTOM;
      }

      if ( self.bodyProperty.value === Body.CUSTOM ) {
        self.customGravityProperty.value = gravity;
      }
    } );

    // change pendulum visibility if number of pendula was changed
    this.numberOfPendulaProperty.link( function( numberOfPendula ) {
      self.pendula.forEach( function( pendulum, pendulumIndex ) {
        pendulum.isVisibleProperty.value = ( numberOfPendula > pendulumIndex );
      } );
    } );
  }

  pendulumLab.register( 'PendulumLabModel', PendulumLabModel );

  return inherit( Object, PendulumLabModel, {
    /**
     * Resets the model.
     * @public
     */
    reset: function() {
      this.bodyProperty.reset();
      this.gravityProperty.reset();
      this.customGravityProperty.reset();
      this.timeSpeedProperty.reset();
      this.numberOfPendulaProperty.reset();
      this.isPlayingProperty.reset();
      this.frictionProperty.reset();
      this.isPeriodTraceVisibleProperty.reset();
      this.energyZoomProperty.reset();

      // reset ruler model
      this.ruler.reset();

      // reset stopwatch model
      this.stopwatch.reset();

      // reset pendulum models
      this.pendula.forEach( function( pendulum ) {
        pendulum.reset();
      } );
    },

    /**
     * Steps the model forward in time.
     * @public
     *
     * @param {number} dt
     */
    step: function( dt ) {
      if ( this.isPlayingProperty.value ) {
        // pick a number as irrational (in the mathematical sense) as possible so that the last digits on the period timer do get stuck to a number
        var periodTimerOffsetFactor = 1.007;

        // For our accuracy guarantees, we cap our DT fairly low. Otherwise the fixed-step model may become inaccurate
        // enough for getting an accurate period timer or speed loss on Jupiter with the shortest length.
        // We apply this BEFORE speed is applied, so that even if we're on a slow device, slow-motion WILL be guaranteed
        // to slow the sim speed down.
        this.modelStep( Math.min( 0.05, dt ) * ( this.timeSpeedProperty.value * periodTimerOffsetFactor ) );
      }
    },

    /**
     * Steps in model time.
     * @private
     *
     * @param {number} dt - change in time measured in seconds
     */
    modelStep: function( dt ) {
      // add time to the stopwatch if it is running
      if ( this.stopwatch.isRunningProperty.value ) {
        this.stopwatch.elapsedTimeProperty.value += dt;
      }

      // loop over the pendula
      for ( var i = 0; i < this.numberOfPendulaProperty.value; i++ ) {
        var pendulum = this.pendula[ i ]; // get the pendulum from the array

        // if the pendulum is moving
        if ( !pendulum.isStationary() ) {
          // prevent infinite motion after friction.
          var dampMotion = ( Math.abs( pendulum.angleProperty.value ) < 1e-3 ) && ( Math.abs( pendulum.angularAccelerationProperty.value ) < 1e-3 ) && ( Math.abs( pendulum.angularVelocityProperty.value ) < 1e-3 );
          if ( dampMotion ) {
            pendulum.angleProperty.value = 0;
            pendulum.angularVelocityProperty.value = 0;
          }
          // step through the pendulum model
          pendulum.step( dt );
        }
      }
    },

    /**
     * Steps forward by a specific amount of time (even if paused).
     * @public
     */
    stepManual: function() {
      this.modelStep( 0.01 ); // advances by 10 ms, see https://github.com/phetsims/pendulum-lab/issues/182
    },

    /**
     * Returns the pendula to rest.
     * @public
     */
    returnPendula: function() {
      //reset the pendula
      this.pendula.forEach( function( pendulum ) {
        pendulum.resetThermalEnergy();
        pendulum.resetMotion();
      } );

      // stop the timer
      if ( this.periodTimer ) {
        this.periodTimer.stop();
      }
    }
  } );
} );
define( function( require ) {
  'use strict';

  // modules
  var AlignBox = require( 'SCENERY/nodes/AlignBox' );
  var AlignGroup = require( 'SCENERY/nodes/AlignGroup' );
  var Dialog = require( 'SUN/Dialog' );
  var HBox = require( 'SCENERY/nodes/HBox' );
  var inherit = require( 'PHET_CORE/inherit' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' );
  var RichText = require( 'SCENERY/nodes/RichText' );
  var Text = require( 'SCENERY/nodes/Text' );
  var VBox = require( 'SCENERY/nodes/VBox' );

  // strings
  var energyLegendString = require( 'string!PENDULUM_LAB/energyLegend' );
  var legendKineticEnergyAbbreviationString = require( 'string!PENDULUM_LAB/legend.kineticEnergyAbbreviation' );
  var legendKineticEnergyString = require( 'string!PENDULUM_LAB/legend.kineticEnergy' );
  var legendPotentialEnergyAbbreviationString = require( 'string!PENDULUM_LAB/legend.potentialEnergyAbbreviation' );
  var legendPotentialEnergyString = require( 'string!PENDULUM_LAB/legend.potentialEnergy' );
  var legendThermalEnergyAbbreviationString = require( 'string!PENDULUM_LAB/legend.thermalEnergyAbbreviation' );
  var legendThermalEnergyString = require( 'string!PENDULUM_LAB/legend.thermalEnergy' );
  var legendTotalEnergyAbbreviationString = require( 'string!PENDULUM_LAB/legend.totalEnergyAbbreviation' );
  var legendTotalEnergyString = require( 'string!PENDULUM_LAB/legend.totalEnergy' );

  /**
   * @constructor
   */
  function EnergyLegendDialog() {
    var abbreviationGroup = new AlignGroup();
    var descriptionGroup = new AlignGroup();

    var content = new VBox( {
      spacing: 15,
      children: [
        {
          abbreviation: legendKineticEnergyAbbreviationString,
          description: legendKineticEnergyString,
          color: PendulumLabConstants.KINETIC_ENERGY_COLOR
        }, {
          abbreviation: legendPotentialEnergyAbbreviationString,
          description: legendPotentialEnergyString,
          color: PendulumLabConstants.POTENTIAL_ENERGY_COLOR
        }, {
          abbreviation: legendThermalEnergyAbbreviationString,
          description: legendThermalEnergyString,
          color: PendulumLabConstants.THERMAL_ENERGY_COLOR
        }, {
          abbreviation: legendTotalEnergyAbbreviationString,
          description: legendTotalEnergyString,
          color: PendulumLabConstants.TOTAL_ENERGY_COLOR
        }
      ].map( function( itemData ) {
        return new HBox( {
          spacing: 20,
          children: [
            new AlignBox( new RichText( itemData.abbreviation, {
              font: PendulumLabConstants.LEGEND_ABBREVIATION_FONT,
              fill: itemData.color,
              maxWidth: 100
            } ), {
              group: abbreviationGroup,
              xAlign: 'left'
            } ),
            new AlignBox( new Text( itemData.description, {
              font: PendulumLabConstants.LEGEND_DESCRIPTION_FONT
            } ), {
              group: descriptionGroup,
              xAlign: 'left',
              maxWidth: 500
            } )
          ]
        } );
      } )
    } );

    Dialog.call( this, content, {
      ySpacing: 20,
      title: new Text( energyLegendString, {
        font: PendulumLabConstants.DIALOG_TITLE_FONT,
        maxWidth: 700
      } )
    } );
  }

  pendulumLab.register( 'EnergyLegendDialog', EnergyLegendDialog );

  return inherit( Dialog, EnergyLegendDialog );
} );
示例#7
0
define( function( require ) {
  'use strict';

  // modules
  var ArrowNode = require( 'SCENERY_PHET/ArrowNode' );
  var Bounds2 = require( 'DOT/Bounds2' );
  var Color = require( 'SCENERY/util/Color' );
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Line = require( 'SCENERY/nodes/Line' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Pendulum = require( 'PENDULUM_LAB/common/model/Pendulum' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' );
  var Property = require( 'AXON/Property' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var Vector2 = require( 'DOT/Vector2' );

  // constants
  var ARROW_HEAD_WIDTH = 12;
  var ARROW_TAIL_WIDTH = 6;
  var ARROW_SIZE_DEFAULT = 25;
  var RECT_SIZE = new Dimension2( 73, 98 );

  /**
   * @constructor
   *
   * @param {Array.<Pendulum>} pendula - Array of pendulum models.
   * @param {ModelViewTransform2} modelViewTransform
   * @param {Object} [options]
   */
  function PendulaNode( pendula, modelViewTransform, options ) {
    var self = this;

    options = _.extend( {
      preventFit: true
    }, options );

    Node.call( this, options );

    var viewOriginPosition = modelViewTransform.modelToViewPosition( Vector2.ZERO );

    // @public {startDrag: {function}, computeDistance: {function}} - To identify how close a draggable object is.
    this.draggableItems = [];

    var pendulumNodes = [];
    var velocityArrows = [];
    var accelerationArrows = [];

    pendula.forEach( function( pendulum, pendulumIndex ) {
      var massToScale = function( mass ) {
        // height/width/depth of mass scale by cube-root to maintain density
        return 0.3 + 0.4 * Math.sqrt( mass / 1.5 );
      };

      // create the visual representation of a rod that joins the fulcrum point to the bob
      // initially set to be vertical
      var solidLine = new Line( 0, 0, 0, modelViewTransform.modelToViewDeltaY( pendulum.lengthProperty.value ), {
        stroke: 'black',
        pickable: false
      } );

      // create the visual representation of a pendulum bob (a rectangle with a string and a line across the rectangle)
      var pendulumRect = new Node( {
        children: [
          new Rectangle( -RECT_SIZE.width / 2, -RECT_SIZE.height / 2, RECT_SIZE.width, RECT_SIZE.height, {
            fill: new LinearGradient( -RECT_SIZE.width / 2, 0, RECT_SIZE.width / 2, 0 ).addColorStop( 0, Color.toColor( pendulum.color ).colorUtilsBrighter( 0.4 ) )
                                                                                       .addColorStop( 0.2, Color.toColor( pendulum.color ).colorUtilsBrighter( 0.9 ) )
                                                                                       .addColorStop( 0.7, pendulum.color )
          } ),
          new Text( ( pendulumIndex + 1 ).toString(), {
            font: PendulumLabConstants.PENDULUM_LABEL_FONT,
            fill: 'white',
            centerY: RECT_SIZE.height / 4,
            centerX: 0,
            pickable: false
          } ),
          new Line( -RECT_SIZE.width / 2, 0, RECT_SIZE.width / 2, 0, {
            stroke: 'black',
            lineCap: 'butt',
            pickable: false
          } )
        ]
      } );

      // create the visual representation of a pendulum (bob + rod)
      var pendulumNode = new Node( {
        cursor: 'pointer',
        children: [
          solidLine,
          pendulumRect
        ]
      } );

      // add velocity arrows if necessary
      if ( options.isVelocityVisibleProperty ) {
        var velocityArrow = new ArrowNode( 0, 0, 0, 0, {
          pickable: false,
          fill: PendulumLabConstants.VELOCITY_ARROW_COLOR,
          tailWidth: ARROW_TAIL_WIDTH,
          headWidth: ARROW_HEAD_WIDTH
        } );
        velocityArrows.push( velocityArrow );

        // no need to unlink, present for the lifetime of the sim
        Property.multilink( [ pendulum.isVisibleProperty, options.isVelocityVisibleProperty, pendulum.velocityProperty ], function( pendulumVisible, velocityVisible, velocity ) {
          velocityArrow.visible = pendulumVisible && velocityVisible;
          // update the size of the arrow
          if ( velocityArrow.visible ) {
            var position = modelViewTransform.modelToViewPosition( pendulum.positionProperty.value );
            velocityArrow.setTailAndTip( position.x,
              position.y,
              position.x + ARROW_SIZE_DEFAULT * velocity.x,
              position.y - ARROW_SIZE_DEFAULT * velocity.y );
          }
        } );
      }


      // add acceleration arrows if necessary
      if ( options.isAccelerationVisibleProperty ) {
        // create acceleration arrow
        var accelerationArrow = new ArrowNode( 0, 0, 0, 0, {
          pickable: false,
          fill: PendulumLabConstants.ACCELERATION_ARROW_COLOR,
          tailWidth: ARROW_TAIL_WIDTH,
          headWidth: ARROW_HEAD_WIDTH
        } );
        accelerationArrows.push( accelerationArrow );

        // no need to unlink, present for the lifetime of the sim
        Property.multilink( [ pendulum.isVisibleProperty, options.isAccelerationVisibleProperty, pendulum.accelerationProperty ], function( pendulumVisible, accelerationVisible, acceleration ) {
          accelerationArrow.visible = pendulumVisible && accelerationVisible;
          if ( accelerationArrow.visible ) {
            var position = modelViewTransform.modelToViewPosition( pendulum.positionProperty.value );
            accelerationArrow.setTailAndTip( position.x,
              position.y,
              position.x + ARROW_SIZE_DEFAULT * acceleration.x,
              position.y - ARROW_SIZE_DEFAULT * acceleration.y );
          }
        } );
      }

      pendulumNodes.push( pendulumNode );

      // add drag events
      var angleOffset;
      var dragListener = new SimpleDragHandler( {
        allowTouchSnag: true,

        // determine the position of where the pendulum is dragged.
        start: function( event ) {
          var dragAngle = modelViewTransform.viewToModelPosition( self.globalToLocalPoint( event.pointer.point ) ).angle + Math.PI / 2;
          angleOffset = pendulum.angleProperty.value - dragAngle;

          pendulum.isUserControlledProperty.value = true;
        },

        // set the angle of the pendulum depending on where it is dragged to.
        drag: function( event ) {
          var dragAngle = modelViewTransform.viewToModelPosition( self.globalToLocalPoint( event.pointer.point ) ).angle + Math.PI / 2;
          var continuousAngle = Pendulum.modAngle( angleOffset + dragAngle );

          // Round angles to nearest degree, see https://github.com/phetsims/pendulum-lab/issues/195
          var roundedAngleDegrees = Util.roundSymmetric( Util.toDegrees( continuousAngle ) );

          // Don't allow snapping to 180, see https://github.com/phetsims/pendulum-lab/issues/195
          if ( Math.abs( roundedAngleDegrees ) === 180 ) {
            roundedAngleDegrees = Util.sign( roundedAngleDegrees ) * 179;
          }

          var roundedAngle = Util.toRadians( roundedAngleDegrees );
          pendulum.angleProperty.value = roundedAngle;
        },

        // release user control
        end: function() {
          pendulum.isUserControlledProperty.value = false;
        }
      } );

      // add a drag listener
      pendulumRect.addInputListener( dragListener );
      self.draggableItems.push( {
        startDrag: dragListener.startDrag.bind( dragListener ),
        computeDistance: function( globalPoint ) {
          if ( pendulum.isUserControlledProperty.value || !pendulum.isVisibleProperty.value ) {
            return Number.POSITIVE_INFINITY;
          }
          else {
            var cursorModelPosition = modelViewTransform.viewToModelPosition( self.globalToLocalPoint( globalPoint ) );
            cursorModelPosition.rotate( -pendulum.angleProperty.value ).add( new Vector2( 0, pendulum.lengthProperty.value ) ); // rotate/length so (0,0) would be mass center
            var massViewWidth = modelViewTransform.viewToModelDeltaX( RECT_SIZE.width * massToScale( pendulum.massProperty.value ) );
            var massViewHeight = modelViewTransform.viewToModelDeltaX( RECT_SIZE.height * massToScale( pendulum.massProperty.value ) );
            var massBounds = new Bounds2( -massViewWidth / 2, -massViewHeight / 2, massViewWidth / 2, massViewHeight / 2 );
            return Math.sqrt( massBounds.minimumDistanceToPointSquared( cursorModelPosition ) );
          }
        }
      } );

      // update pendulum rotation, pendulum.angleProperty.value is radians
      // we are using an inverted modelViewTransform, hence we multiply the view angle by minus one
      pendulum.angleProperty.link( function( angle ) {
        pendulumNode.rotation = -angle;
        pendulumNode.translation = viewOriginPosition;
      } );

      // update pendulum components position
      pendulum.lengthProperty.link( function( length ) {
        var viewPendulumLength = modelViewTransform.modelToViewDeltaX( length );

        pendulumRect.setY( viewPendulumLength );
        solidLine.setY2( viewPendulumLength );
      } );

      // update rectangle size
      pendulum.massProperty.link( function( mass ) {
        pendulumRect.setScaleMagnitude( massToScale( mass ) );
      } );

      // update visibility
      pendulum.isVisibleProperty.linkAttribute( pendulumNode, 'visible' );
    } );

    this.children = pendulumNodes.concat( velocityArrows ).concat( accelerationArrows );
  }

  pendulumLab.register( 'PendulaNode', PendulaNode );

  return inherit( Node, PendulaNode );
} );
define( function( require ) {
  'use strict';

  // modules
  var ClosestDragListener = require( 'SUN/ClosestDragListener' );
  var GlobalControlPanel = require( 'PENDULUM_LAB/common/view/GlobalControlPanel' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var PendulaNode = require( 'PENDULUM_LAB/common/view/PendulaNode' );
  var PendulumControlPanel = require( 'PENDULUM_LAB/common/view/PendulumControlPanel' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' );
  var PendulumLabRulerNode = require( 'PENDULUM_LAB/common/view/PendulumLabRulerNode' );
  var PeriodTraceNode = require( 'PENDULUM_LAB/common/view/PeriodTraceNode' );
  var Plane = require( 'SCENERY/nodes/Plane' );
  var PlaybackControlsNode = require( 'PENDULUM_LAB/common/view/PlaybackControlsNode' );
  var ProtractorNode = require( 'PENDULUM_LAB/common/view/ProtractorNode' );
  var ResetAllButton = require( 'SCENERY_PHET/buttons/ResetAllButton' );
  var ScreenView = require( 'JOIST/ScreenView' );
  var StopwatchNode = require( 'PENDULUM_LAB/common/view/StopwatchNode' );
  var ToolsPanel = require( 'PENDULUM_LAB/common/view/ToolsPanel' );
  var VBox = require( 'SCENERY/nodes/VBox' );

  /**
   * @constructor
   *
   * @param {PendulumLabModel} model
   * @param {ModelViewTransform2} modelViewTransform
   */
  function PendulumLabScreenView( model, options ) {
    ScreenView.call( this );

    // @private {PendulumLabModel}
    this.model = model;

    options = _.extend( {
      hasGravityTweakers: false,
      hasPeriodTimer: false
    }, options );

    var modelViewTransform = PendulumLabConstants.MODEL_VIEW_TRANSFORM;

    var pendulaNode = new PendulaNode( model.pendula, modelViewTransform, {
      isAccelerationVisibleProperty: model.isAccelerationVisibleProperty,
      isVelocityVisibleProperty: model.isVelocityVisibleProperty
    } );

    // create drag listener for the pendula
    var backgroundDragNode = new Plane();
    var dragListener = new ClosestDragListener( 0.15, 0 ); // 15cm from mass is OK for touch
    pendulaNode.draggableItems.forEach( function( draggableItem ) {
      dragListener.addDraggableItem( draggableItem );
    } );
    backgroundDragNode.addInputListener( dragListener );

    // @private {PeriodTraceNode}
    this.firstPeriodTraceNode = new PeriodTraceNode( model.pendula[ 0 ], modelViewTransform );
    this.secondPeriodTraceNode = new PeriodTraceNode( model.pendula[ 1 ], modelViewTransform );

    // create protractor node
    var protractorNode = new ProtractorNode( model.pendula, modelViewTransform );

    // create a node to keep track of combo box
    var popupLayer = new Node();

    var pendulumControlPanel = new PendulumControlPanel( model.pendula, model.numberOfPendulaProperty );
    var globalControlPanel = new GlobalControlPanel( model, popupLayer, !!options.hasGravityTweakers );

    // @protected
    this.rightPanelsContainer = new VBox( {
      spacing: PendulumLabConstants.PANEL_PADDING,
      children: [
        pendulumControlPanel,
        globalControlPanel
      ],
      right: this.layoutBounds.right - PendulumLabConstants.PANEL_PADDING,
      top: this.layoutBounds.top + PendulumLabConstants.PANEL_PADDING
    } );

    // create tools control panel (which controls the visibility of the ruler and stopwatch)
    var toolsControlPanelNode = new ToolsPanel( model.ruler.isVisibleProperty,
                                                model.stopwatch.isVisibleProperty,
                                                model.isPeriodTraceVisibleProperty,
                                                options.hasPeriodTimer, {
      maxWidth: 180,
      left: this.layoutBounds.left + PendulumLabConstants.PANEL_PADDING,
      bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING
    } );

    // @protected {Node}
    this.toolsControlPanelNode = toolsControlPanelNode;

    // create pendulum system control panel (controls the length and mass of the pendula)
    var playbackControls = new PlaybackControlsNode( model.numberOfPendulaProperty,
                                                     model.isPlayingProperty,
                                                     model.timeSpeedProperty,
                                                     model.stepManual.bind( model ),
                                                     model.returnPendula.bind( model ), {
      x: this.layoutBounds.centerX,
      bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING
    } );

    // create reset all button
    var resetAllButton = new ResetAllButton( {
      listener: model.reset.bind( model ),
      right: this.layoutBounds.right - PendulumLabConstants.PANEL_PADDING,
      bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING
    } );

    // create ruler node
    var rulerNode = new PendulumLabRulerNode( model.ruler, modelViewTransform, this.layoutBounds );
    rulerNode.left = this.layoutBounds.left + PendulumLabConstants.PANEL_PADDING;
    rulerNode.top = this.layoutBounds.top + PendulumLabConstants.PANEL_PADDING;
    model.ruler.setInitialLocationValue( rulerNode.center );

    // @protected
    this.rulerNode = rulerNode;

    // create timer node
    var stopwatchNode = new StopwatchNode( model.stopwatch, this.layoutBounds );
    stopwatchNode.bottom = rulerNode.bottom;
    stopwatchNode.right = Math.max( 167.75, toolsControlPanelNode.right ); // If we are only on this screen
    model.stopwatch.setInitialLocationValue( stopwatchNode.center );

    // @protected
    this.stopwatchNode = stopwatchNode;

    // @protected
    this.arrowsPanelLayer = new Node();
    this.energyGraphLayer = new Node();
    this.periodTimerLayer = new Node();

    var leftFloatingLayer = new Node( {
      children: [
        this.energyGraphLayer, this.arrowsPanelLayer, toolsControlPanelNode
      ]
    } );
    var rightFloatingLayer = new Node( {
      children: [
        this.rightPanelsContainer,
        resetAllButton,
        popupLayer
      ]
    } );

    // Layout for https://github.com/phetsims/pendulum-lab/issues/98
    this.visibleBoundsProperty.lazyLink( function( visibleBounds ) {
      var dx = -visibleBounds.x;
      dx = Math.min( 200, dx );
      leftFloatingLayer.x = -dx;
      rightFloatingLayer.x = dx;
      // set the drag bounds of the ruler and stopwatch
      rulerNode.movableDragHandler.setDragBounds( visibleBounds.erodedXY( rulerNode.width / 2, rulerNode.height / 2 ) );
      stopwatchNode.movableDragHandler.setDragBounds( visibleBounds.erodedXY( stopwatchNode.width / 2, stopwatchNode.height / 2 ) );
    } );

    this.children = [
      backgroundDragNode,
      protractorNode,
      leftFloatingLayer,
      rightFloatingLayer,
      playbackControls,
      this.firstPeriodTraceNode,
      this.secondPeriodTraceNode,
      pendulaNode,
      rulerNode,
      this.periodTimerLayer,
      stopwatchNode
    ];
  }

  pendulumLab.register( 'PendulumLabScreenView', PendulumLabScreenView );

  return inherit( ScreenView, PendulumLabScreenView, {
    /**
     * Steps the view.
     * @public
     *
     * @param {number} dt
     */
    step: function( dt ) {
      if ( this.model.isPlayingProperty.value ) {
        this.firstPeriodTraceNode.step( dt );
        this.secondPeriodTraceNode.step( dt );
      }
    }
  } );
} );
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var MovableDragHandler = require( 'SCENERY_PHET/input/MovableDragHandler' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' );
  var RulerNode = require( 'SCENERY_PHET/RulerNode' );

  // strings
  var rulerUnitsString = require( 'string!PENDULUM_LAB/rulerUnits' );

  // constants
  var RULER_HEIGHT = 34;
  var TICK_INTERVAL = 5; // tick interval in cm

  /**
   * @constructor
   *
   * @param {Ruler} ruler - Model for ruler.
   * @param {ModelViewTransform2} modelViewTransform
   * @param {Bounds2} layoutBounds - Bounds of screen view
   */
  function PendulumLabRulerNode( ruler, modelViewTransform, layoutBounds ) {
    var self = this;

    // create tick labels
    var tickLabel;
    var rulerTicks = [ '' ]; // zero tick is not labeled
    for ( var currentTick = TICK_INTERVAL; currentTick < ruler.length * 100; currentTick += TICK_INTERVAL ) {
      // if the current tick is a multiple of twice the Tick interval then label it as such otherwise it is not labeled.
      tickLabel = currentTick % ( 2 * TICK_INTERVAL ) ? '' : currentTick.toString();
      rulerTicks.push( tickLabel );
    }
    rulerTicks.push( '' ); // last tick is not labeled

    // define ruler params in view coordinates
    var rulerWidth = modelViewTransform.modelToViewDeltaX( ruler.length );
    var tickWidth = rulerWidth / ( rulerTicks.length - 1 );

    RulerNode.call( this, rulerWidth, RULER_HEIGHT, tickWidth, rulerTicks, rulerUnitsString, {
      backgroundFill: 'rgb( 237, 225, 121 )',
      cursor: 'pointer',
      insetsWidth: 0,
      majorTickFont: PendulumLabConstants.RULER_FONT,
      majorTickHeight: 12,
      minorTickHeight: 6,
      unitsFont: PendulumLabConstants.RULER_FONT,
      unitsMajorTickIndex: rulerTicks.length - 3,
      minorTicksPerMajorTick: 4,
      tickMarksOnBottom: false
    } );

    // make it a vertical ruler
    this.rotate( Math.PI / 2 );

    // @public
    this.movableDragHandler = new MovableDragHandler( ruler.locationProperty, {
      dragBounds: layoutBounds.erodedXY( this.width / 2, this.height / 2 )
    } );

    // add drag and drop events
    this.addInputListener( this.movableDragHandler );

    // add update of node location
    ruler.locationProperty.lazyLink( function( location ) {
      // because it's initially null, and will be null on a reset
      if ( location ) {
        self.center = location;
      }
    } );

    // set visibility observer
    ruler.isVisibleProperty.linkAttribute( this, 'visible' );
  }

  pendulumLab.register( 'PendulumLabRulerNode', PendulumLabRulerNode );

  return inherit( RulerNode, PendulumLabRulerNode );
} );
示例#10
0
define( function( require ) {
  'use strict';

  // modules
  var BooleanProperty = require( 'AXON/BooleanProperty' );
  var DynamicProperty = require( 'AXON/DynamicProperty' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' );
  var PendulumNumberControl = require( 'PENDULUM_LAB/common/view/PendulumNumberControl' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );

  // strings
  var frictionString = require( 'string!PENDULUM_LAB/friction' );
  var lotsString = require( 'string!PENDULUM_LAB/lots' );
  var noneString = require( 'string!PENDULUM_LAB/none' );

  /**
   * Converts the numerical value of the slider to friction, does not assign to friction property
   * @private
   *
   * @param {number} sliderValue
   * @returns {number}
   */
  function sliderValueToFriction( sliderValue ) {
    return 0.0005 * ( Math.pow( 2, sliderValue ) - 1 );
  }

  /**
   * Converts the numerical value of the friction to a slider value, does not assign to slider property
   * @private
   *
   * @param {number}friction
   * @returns {number}
   */
  function frictionToSliderValue( friction ) {
    return Util.roundSymmetric( Math.log( friction / 0.0005 + 1 ) / Math.LN2 );
  }

  /**
   * @constructor
   *
   * @param {Property.<number>} frictionProperty - Property to update by slider.
   * @param {Range} frictionRange - Possible range of frictionProperty value.
   * @param {Object} [options]
   */
  function FrictionSliderNode( frictionProperty, frictionRange, options ) {

    var sliderValueProperty = new DynamicProperty( new Property( frictionProperty ), {
      bidirectional: true,
      map: frictionToSliderValue,
      inverseMap: sliderValueToFriction
    } );

    // range the slider can have
    var sliderValueRange = new Range( frictionToSliderValue( frictionRange.min ), frictionToSliderValue( frictionRange.max ) );

    //TODO #210 replace '{0}' with SunConstants.VALUE_NAMED_PLACEHOLDER
    var numberControl = new PendulumNumberControl( frictionString, sliderValueProperty, sliderValueRange, '{0}', 'rgb(50,145,184)', {
      hasReadoutProperty: new BooleanProperty( false ),
      excludeTweakers: true,
      sliderPadding: 14,
      sliderOptions: {
        thumbFill: '#00C4DF',
        thumbFillHighlighted: '#71EDFF',
        minorTickLength: 5,
        majorTickLength: 10,
        constrainValue: function( value ) {
          return Util.roundSymmetric( value );
        },
        
        majorTicks: [
          {
            value: sliderValueRange.min,
            label: new Text( noneString, { font: PendulumLabConstants.TICK_FONT, maxWidth: 50 } )
          }, {
            value: sliderValueRange.getCenter(),
            label: null
          }, {
            value: sliderValueRange.max,
            label: new Text( lotsString, { font: PendulumLabConstants.TICK_FONT, maxWidth: 50 } )
          }
        ],

        minorTickSpacing: sliderValueRange.getLength() / 10
      }
    } );

    // describes the panel box containing the friction slider
    Node.call( this, _.extend( {
      children: [ numberControl ]
    }, options ) );
  }

  pendulumLab.register( 'FrictionSliderNode', FrictionSliderNode );

  return inherit( Node, FrictionSliderNode );
} );
示例#11
0
define( function( require ) {
  'use strict';

  // modules
  var BooleanProperty = require( 'AXON/BooleanProperty' );
  var Emitter = require( 'AXON/Emitter' );
  var inherit = require( 'PHET_CORE/inherit' );
  var NumberProperty = require( 'AXON/NumberProperty' );
  var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' );
  var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' );
  var PeriodTrace = require( 'PENDULUM_LAB/common/model/PeriodTrace' );
  var Property = require( 'AXON/Property' );
  var Range = require( 'DOT/Range' );
  var Util = require( 'DOT/Util' );
  var Vector2 = require( 'DOT/Vector2' );
  var Vector2Property = require( 'DOT/Vector2Property' );

  // constants
  var TWO_PI = Math.PI * 2;

  // scratch vector for convenience
  var scratchVector = new Vector2( 0, 0 );

  /**
   * @constructor
   *
   * @param {number} index - Which pendulum in a system is this?
   * @param {number} mass - mass of pendulum, kg.
   * @param {number} length - length of pendulum, m.
   * @param {boolean} isVisible - Initial visibility of pendulum.
   * @param {Property.<number>} gravityProperty - Property with current gravity value.
   * @param {Property.<number>} frictionProperty - Property with current friction value.
   * @param {Property.<boolean>} isPeriodTraceVisibleProperty - Flag property to track checkbox value of period trace visibility.
   * @param {boolean} hasPeriodTimer
   */
  function Pendulum( index, mass, length, isVisible, gravityProperty, frictionProperty, isPeriodTraceVisibleProperty, hasPeriodTimer ) {
    var self = this;

    // @public {number}
    this.index = index;

    // @public {Property.<number>} - Length of the pendulum (in meters)
    this.lengthProperty = new NumberProperty( length );

    // @public {Property.<number>} - Mass of the pendulum (in kilograms)
    this.massProperty = new NumberProperty( mass );

    // @public {Property.<number>} - Angle in radians (0 is straight down, positive is to the right)
    this.angleProperty = new NumberProperty( 0 );

    // @public {Property.<number>} - Angular velocity (in radians/second)
    this.angularVelocityProperty = new NumberProperty( 0 );

    // @public {boolean}
    this.hasPeriodTimer = hasPeriodTimer;

    /*---------------------------------------------------------------------------*
    * Derived variables
    *----------------------------------------------------------------------------*/

    // @public {Property.<number>} - Angular acceleration in rad/s^2
    this.angularAccelerationProperty = new NumberProperty( 0 );

    // @public - Position from the rotation point
    this.positionProperty = new Vector2Property( Vector2.ZERO );

    // @public
    this.velocityProperty = new Vector2Property( Vector2.ZERO );

    // @public
    this.accelerationProperty = new Vector2Property( Vector2.ZERO );

    // @public {Property.<number>} - In Joules
    this.kineticEnergyProperty = new NumberProperty( 0 );

    // @public {Property.<number>} - In Joules
    this.potentialEnergyProperty = new NumberProperty( 0 );

    // @public {Property.<number>} - In Joules
    this.thermalEnergyProperty = new NumberProperty( 0 );

    // @public {Property.<boolean>} - Whether the pendulum is currently being dragged.
    this.isUserControlledProperty = new BooleanProperty( false );

    // @public {Property.<boolean>} - Whether the pendulum tick is visible on the protractor.
    this.isTickVisibleProperty = new BooleanProperty( false );

    // @public {Property.<boolean>} - Whether the entire pendulum is visible or not
    this.isVisibleProperty = new BooleanProperty( false );

    // save link to global properties
    // @private
    this.gravityProperty = gravityProperty;
    this.frictionProperty = frictionProperty;

    // @public
    this.stepEmitter = new Emitter( { validators: [ { valueType: 'number' } ] } );
    this.userMovedEmitter = new Emitter();
    this.crossingEmitter = new Emitter( { validators: [ { valueType: 'number' }, { valueType: 'boolean' } ] } );
    this.peakEmitter = new Emitter( { validators: [ { valueType: 'number' } ] } );
    this.resetEmitter = new Emitter();

    // default color for this pendulum
    // @public (read-only)
    this.color = PendulumLabConstants.PENDULUM_COLORS[ index ]; // {string}

    // @public {Range} (read-only)
    this.lengthRange = new Range( 0.1, 1.0 );

    // @public {Range} (read-only)
    this.massRange = new Range( 0.1, 1.50 );

    // @public {PeriodTrace}
    this.periodTrace = new PeriodTrace( this );

    // If it NOT repeatable, the PeriodTimer type will control the visibility.
    if ( !hasPeriodTimer ) {
      Property.multilink( [ isPeriodTraceVisibleProperty, this.isVisibleProperty ], function( isPeriodTraceVisible, isVisible ) {
        self.periodTrace.isVisibleProperty.value = isPeriodTraceVisible && isVisible;
      } );
    }

    // make tick on protractor visible after first drag
    this.isUserControlledProperty.lazyLink( function( isUserControlled ) {
      if ( isUserControlled ) {
        self.isTickVisibleProperty.value = true; // Seems like an UI-specific issue, not model

        self.angularVelocityProperty.value = 0;
        self.updateDerivedVariables( false );

        // Clear thermal energy on a drag, see https://github.com/phetsims/pendulum-lab/issues/196
        self.thermalEnergyProperty.value = 0;
      }
    } );

    // make the angle value visible after the first drag
    this.angleProperty.lazyLink( function() {
      if ( self.isUserControlledProperty.value ) {
        self.updateDerivedVariables( false );
        self.userMovedEmitter.emit();
      }
    } );

    // update the angular velocity when the length changes
    this.lengthProperty.lazyLink( function( newLength, oldLength ) {
      self.angularVelocityProperty.value = self.angularVelocityProperty.value * oldLength / newLength;
      self.updateDerivedVariables( false ); // preserve thermal energy
    } );

    this.updateListener = this.updateDerivedVariables.bind( this, false ); // don't add thermal energy on these callbacks
    this.massProperty.lazyLink( this.updateListener );
    gravityProperty.lazyLink( this.updateListener );
  }

  pendulumLab.register( 'Pendulum', Pendulum );

  return inherit( Object, Pendulum, {
    /**
     * Function that returns the instantaneous angular acceleration
     * @private
     *
     * @param {number} theta - angular position
     * @param {number} omega - angular velocity
     * @returns {number}
     */
    omegaDerivative: function( theta, omega ) {
      return -this.frictionTerm( omega ) - ( this.gravityProperty.value / this.lengthProperty.value ) * Math.sin( theta );
    },

    /**
     * Function that returns the tangential drag force on the pendulum per unit mass per unit length
     * The friction term has units of angular acceleration.
     * The friction has a linear and quadratic component (with speed)
     * @private
     *
     * @param {number} omega - the angular velocity of the pendulum
     * @returns {number}
     */
    frictionTerm: function( omega ) {
      return this.frictionProperty.value * this.lengthProperty.value / Math.pow( this.massProperty.value, 1 / 3 ) * omega * Math.abs( omega ) +
             this.frictionProperty.value / Math.pow( this.massProperty.value, 2 / 3 ) * omega;
    },

    /**
     * Stepper function for the pendulum model.
     * It uses a Runge-Kutta approach to solve the angular differential equation
     * @public
     *
     * @param {number} dt
     */
    step: function( dt ) {
      var theta = this.angleProperty.value;

      var omega = this.angularVelocityProperty.value;

      var numSteps = Math.max( 7, dt * 120 );

      // 10 iterations typically maintains about ~11 digits of precision for total energy
      for ( var i = 0; i < numSteps; i++ ) {
        var step = dt / numSteps;

        // Runge Kutta (order 4), where the derivative of theta is omega.
        var k1 = omega * step;
        var l1 = this.omegaDerivative( theta, omega ) * step;
        var k2 = ( omega + 0.5 * l1 ) * step;
        var l2 = this.omegaDerivative( theta + 0.5 * k1, omega + 0.5 * l1 ) * step;
        var k3 = ( omega + 0.5 * l2 ) * step;
        var l3 = this.omegaDerivative( theta + 0.5 * k2, omega + 0.5 * l2 ) * step;
        var k4 = ( omega + l3 ) * step;
        var l4 = this.omegaDerivative( theta + k3, omega + l3 ) * step;
        var newTheta = Pendulum.modAngle( theta + ( k1 + 2 * k2 + 2 * k3 + k4 ) / 6 );
        var newOmega = omega + ( l1 + 2 * l2 + 2 * l3 + l4 ) / 6;

        // did the pendulum crossed the vertical axis (from below)
        // is the pendulum going from left to right or vice versa, or (is the pendulum on the vertical axis and changed position )
        if ( ( newTheta * theta < 0 ) || ( newTheta === 0 && theta !== 0 ) ) {
          this.cross( i * step, ( i + 1 ) * step, newOmega > 0, theta, newTheta );
        }

        // did the pendulum reach a turning point
        // is the pendulum changing is speed from left to right or is the angular speed zero but wasn't zero on the last update
        if ( ( newOmega * omega < 0 ) || ( newOmega === 0 && omega !== 0 ) ) {
          this.peak( theta, newTheta );
        }

        theta = newTheta;
        omega = newOmega;
      }

      // update the angular variables
      this.angleProperty.value = theta;
      this.angularVelocityProperty.value = omega;

      // update the derived variables, taking into account the transfer to thermal energy if friction is present
      this.updateDerivedVariables( this.frictionProperty.value > 0 );

      this.stepEmitter.emit( dt );
    },

    /**
     * Function that emits when the pendulum is crossing the equilibrium point (theta=0)
     * Given that the time step is finite, we attempt to do a linear interpolation, to find the
     * precise time at which the pendulum cross the vertical.
     * @private
     *
     * @param {number} oldDT
     * @param {number} newDT
     * @param {boolean} isPositiveDirection
     * @param {number} oldTheta
     * @param {number} newTheta
     */
    cross: function( oldDT, newDT, isPositiveDirection, oldTheta, newTheta ) {
      // If we crossed near oldTheta, our crossing DT is near oldDT. If we crossed near newTheta, our crossing DT is close
      // to newDT.
      var crossingDT = Util.linear( oldTheta, newTheta, oldDT, newDT, 0 );

      this.crossingEmitter.emit( crossingDT, isPositiveDirection );
    },

    /**
     * Sends a signal that the peak angle (turning angle) has been reached
     * It sends the value of the peak angle
     * @private
     *
     * @param {number} oldTheta
     * @param {number} newTheta
     */
    peak: function( oldTheta, newTheta ) {
      // a slightly better estimate is turningAngle =  ( oldTheta + newTheta ) / 2 + (dt/2)*(oldOmega^2+newOmega^2)/(oldOmega-newOmega)
      var turningAngle = ( oldTheta + newTheta > 0 ) ? Math.max( oldTheta, newTheta ) : Math.min( oldTheta, newTheta );
      this.peakEmitter.emit( turningAngle );
    },

    /**
     * Given the angular position and velocity, this function updates derived variables :
     * namely the various energies( kinetic, thermal, potential and total energy)
     * and the linear variables (position, velocity, acceleration) of the pendulum
     * @private
     *
     * @param {boolean} energyChangeToThermal - is Friction present in the model
     */
    updateDerivedVariables: function( energyChangeToThermal ) {
      var speed = Math.abs( this.angularVelocityProperty.value ) * this.lengthProperty.value;

      this.angularAccelerationProperty.value = this.omegaDerivative( this.angleProperty.value, this.angularVelocityProperty.value );
      var height = this.lengthProperty.value * ( 1 - Math.cos( this.angleProperty.value ) );

      var oldKineticEnergy = this.kineticEnergyProperty.value;
      this.kineticEnergyProperty.value = 0.5 * this.massProperty.value * speed * speed;

      var oldPotentialEnergy = this.potentialEnergyProperty.value;
      this.potentialEnergyProperty.value = this.massProperty.value * this.gravityProperty.value * height;

      if ( energyChangeToThermal ) {
        this.thermalEnergyProperty.value += ( oldKineticEnergy + oldPotentialEnergy ) - ( this.kineticEnergyProperty.value + this.potentialEnergyProperty.value );
      }

      this.positionProperty.value = Vector2.createPolar( this.lengthProperty.value, this.angleProperty.value - Math.PI / 2 );
      this.velocityProperty.value = Vector2.createPolar( this.angularVelocityProperty.value * this.lengthProperty.value, this.angleProperty.value ); // coordinate frame -pi/2, but perpendicular +pi/2

      // add up net forces for the acceleration

      // tangential friction
      this.accelerationProperty.value = Vector2.createPolar( -this.frictionTerm( this.angularVelocityProperty.value ) / this.massProperty.value, this.angleProperty.value );
      // tangential gravity
      this.accelerationProperty.value.add( scratchVector.setPolar( -this.gravityProperty.value * Math.sin( this.angleProperty.value ), this.angleProperty.value ) );
      // radial (centripetal acceleration)
      this.accelerationProperty.value.add( scratchVector.setPolar( this.lengthProperty.value * this.angularVelocityProperty.value * this.angularVelocityProperty.value, this.angleProperty.value + Math.PI / 2 ) );

      this.velocityProperty.notifyListenersStatic();
      this.accelerationProperty.notifyListenersStatic();
    },

    /**
     * Reset all the properties of this model.
     * @public
     */
    reset: function() {
      // Note: We don't reset isVisibleProperty, since it is controlled externally.
      this.lengthProperty.reset();
      this.massProperty.reset();
      this.angleProperty.reset();
      this.angularVelocityProperty.reset();
      this.angularAccelerationProperty.reset();
      this.positionProperty.reset();
      this.velocityProperty.reset();
      this.accelerationProperty.reset();
      this.kineticEnergyProperty.reset();
      this.potentialEnergyProperty.reset();
      this.thermalEnergyProperty.reset();
      this.isUserControlledProperty.reset();
      this.isTickVisibleProperty.reset();

      this.updateDerivedVariables( false );
    },

    /**
     * Function that determines if the pendulum is stationary, i.e. is controlled by the user or not moving
     * @public
     *
     * @returns {boolean}
     */
    isStationary: function() {
      return this.isUserControlledProperty.value || ( this.angleProperty.value === 0 &&
                                                      this.angularVelocityProperty.value === 0 &&
                                                      this.angularAccelerationProperty.value === 0 );
    },

    /**
     * Functions returns an approximate period of the pendulum
     * The so-called small angle approximation is a lower bound to the true period in absence of friction
     * This function is currently used to fade out the path of the period trace
     * @public
     *
     * @returns {number}
     */
    getApproximatePeriod: function() {
      return 2 * Math.PI * Math.sqrt( this.lengthProperty.value / this.gravityProperty.value );
    },

    /**
     * Resets the motion of the Pendulum
     * @public
     */
    resetMotion: function() {
      this.angleProperty.reset();
      this.angularVelocityProperty.reset();

      // ticks are initially invisible
      this.isTickVisibleProperty.reset();

      this.periodTrace.resetPathPoints();

      this.updateDerivedVariables( false );

      this.resetEmitter.emit();
    },

    /**
     * Resets the thermal energy to zero
     * @public
     */
    resetThermalEnergy: function() {
      this.thermalEnergyProperty.reset();
    }
  }, {
    /**
     * Takes our angle modulo 2pi between -pi and pi.
     * @public
     *
     * @param {number} angle
     * @returns {number}
     */
    modAngle: function( angle ) {
      angle = angle % TWO_PI;

      if ( angle < -Math.PI ) {
        angle += TWO_PI;
      }
      if ( angle > Math.PI ) {
        angle -= TWO_PI;
      }

      return angle;
    }
  } );
} );