Esempio n. 1
0
    debugPositions: function( johnTravoltageView ) {
      johnTravoltageView.touchArea = Shape.rectangle( 0, 0, 1000, 1000 );
      johnTravoltageView.mouseArea = Shape.rectangle( 0, 0, 1000, 1000 );
      var string = '';
      johnTravoltageView.addInputListener( {
        down: function( event ) {
          var pt = event.pointer.point;
          var global = johnTravoltageView.globalToLocalPoint( pt );
          var a = 'new Vector2(' + global.x + ',' + global.y + '),\n';

          string = string + a;
          console.log( string );
        }
      } );
    },
Esempio n. 2
0
  /**
   * The slider thumb, a rounded rectangle with a horizontal line through its center. Origin is at the thumb's geometric center.
   * @param {Dimension2} size
   * @param {Property.<number>} property
   * @param {Range} valueRange
   * @param {number} decimalPlaces
   * @param {Range} positionRange
   * @constructor
   */
  function Thumb( size, property, valueRange, decimalPlaces, positionRange ) {

    var thisNode = this;
    Node.call( this );

    // nodes
    var bodyNode = new Rectangle( -size.width / 2, -size.height / 2, size.width, size.height, 10, 10,
      { fill: THUMB_NORMAL_COLOR, stroke: THUMB_STROKE_COLOR, lineWidth: 1 } );
    var centerLineNode = new Path( Shape.lineSegment( -( size.width / 2 ) + 3, 0, ( size.width / 2 ) - 3, 0 ),
      { stroke: THUMB_CENTER_LINE_COLOR } );

    // rendering order
    thisNode.addChild( bodyNode );
    thisNode.addChild( centerLineNode );

    // touch area
    var touchXMargin = 0 * bodyNode.width; // thumb seems wide enough, so zero for now
    var touchYMargin = 1 * bodyNode.height; // expand height since thumb is not very tall and drag direction is vertical
    bodyNode.touchArea = Shape.rectangle( bodyNode.left - touchXMargin, bodyNode.top - touchYMargin,
      bodyNode.width + ( 2 * touchXMargin ), bodyNode.height + ( 2 * touchYMargin ) );

    // interactivity
    thisNode.cursor = 'pointer';
    thisNode.addInputListener( new ThumbDragHandler( thisNode, property, valueRange, decimalPlaces, positionRange ) );
    bodyNode.addInputListener( new FillHighlightListener( THUMB_NORMAL_COLOR, THUMB_HIGHLIGHT_COLOR ) );
  }
  /**
   * @param {Function} callback function to be called when the user presses the clear thermal button.
   * @param {Skater} skater the model's skater model
   * @param {*}options
   * @constructor
   */
  function ClearThermalButton( callback, skater, options ) {
    var clearThermalButton = this;
    options = _.extend( {
      cursor: 'pointer',
      phetioID: null
    }, options );

    var icon = new Image( trashCanImage, { scale: 0.22 } );

    RectangularPushButton.call( this, {
      content: icon,
      baseColor: new Color( 230, 230, 240 ),
      disabledBaseColor: 'white',
      cornerRadius: 6,
      listener: callback,
      buttonAppearanceStrategy: RectangularButtonView.flatAppearanceStrategy,
      xMargin: 7,
      yMargin: 3,
      phetioID: options.phetioID
    } );
    skater.allowClearingThermalEnergyProperty.link(
      function( allowClearingThermalEnergy ) {
        icon.image = allowClearingThermalEnergy ? trashCanImage : trashCanGrayImage;
        icon.opacity = allowClearingThermalEnergy ? 1 : 0.3;
        clearThermalButton.pickable = allowClearingThermalEnergy;
      } );
    this.mouseArea = this.touchArea = Shape.rectangle( icon.bounds.minX, icon.bounds.minY, icon.bounds.width, icon.bounds.height );
    this.mutate( options );
  }
Esempio n. 4
0
 solution.volumeProperty.link( function( volume ) {
   if ( volume === 0 ) {
     thisNode.clipArea = null;
   }
   else {
     var solutionHeight = beakerBounds.getHeight() * volume / beaker.volume;
     thisNode.clipArea = Shape.rectangle( beakerBounds.minX, beakerBounds.maxY - solutionHeight, beakerBounds.getWidth(), solutionHeight );
   }
   thisNode.moleculesNode.invalidatePaint(); //WORKAROUND: #25, scenery#200
 } );
Esempio n. 5
0
  QUnit.test( 'ES5 Setter / Getter tests', function( assert ) {
    var node = new Path( null );
    var fill = '#abcdef';
    node.fill = fill;
    assert.equal( node.fill, fill );
    assert.equal( node.getFill(), fill );

    var otherNode = new Path( Shape.rectangle( 0, 0, 10, 10 ), { fill: fill } );

    assert.equal( otherNode.fill, fill );
  } );
Esempio n. 6
0
  QUnit.test( 'Sceneless node handling', function( assert ) {
    var a = new Path( null );
    var b = new Path( null );
    var c = new Path( null );

    a.setShape( Shape.rectangle( 0, 0, 20, 20 ) );
    c.setShape( Shape.rectangle( 10, 10, 30, 30 ) );

    a.addChild( b );
    b.addChild( c );

    a.validateBounds();

    a.removeChild( b );
    c.addChild( a );

    b.validateBounds();

    assert.ok( true, 'so we have at least 1 test in this set' );
  } );
Esempio n. 7
0
 debugLineSegments: function( johnTravoltageView ) {
   johnTravoltageView.touchArea = Shape.rectangle( 0, 0, 1000, 1000 );
   johnTravoltageView.mouseArea = Shape.rectangle( 0, 0, 1000, 1000 );
   var string = '';
   var p1 = null;
   johnTravoltageView.addInputListener( {
     down: function( event ) {
       var pt = event.pointer.point;
       var global = johnTravoltageView.globalToLocalPoint( pt );
       if ( p1 ) {
         string = string + 'new LineSegment(' + p1.x + ',' + p1.y + ',' + global.x + ',' + global.y + '),\n';
         console.log( string );
         p1 = null;
       }
       else {
         p1 = global;
       }
     }
   } );
 }
  /**
   * Meter probe, origin at center of crosshairs.
   * @param {Movable} probe
   * @param {ModelViewTransform2} modelViewTransform
   * @param {Node} solutionNode
   * @param {Node} dropperFluidNode
   * @param {Node} waterFluidNode
   * @param {Node} drainFluidNode
   * @constructor
   */
  function ProbeNode( probe, modelViewTransform, solutionNode, dropperFluidNode, waterFluidNode, drainFluidNode ) {

    var thisNode = this;
    Node.call( thisNode, {
      cursor: 'pointer'
    } );

    // probe image file
    var imageNode = new Image( probeImage );
    thisNode.addChild( imageNode );
    var radius = imageNode.height / 2; // assumes that image height defines the radius
    imageNode.x = -imageNode.width + radius; // assumes the image is oriented with probe 'handle' on left
    imageNode.y = -radius; // assumes the image is oriented horizontally

    // probe location
    probe.locationProperty.link( function( location ) {
      thisNode.translation = modelViewTransform.modelToViewPosition( location );
    } );

    // touch area
    var dx = 0.25 * imageNode.width;
    var dy = 0.25 * imageNode.height;
    thisNode.touchArea = Shape.rectangle( imageNode.x - dx, imageNode.y - dy, imageNode.width + dx + dx, imageNode.height + dy + dy );

    // drag handler
    thisNode.addInputListener( new MovableDragHandler( probe, modelViewTransform ) );

    var isInNode = function( node ) {
      return node.getBounds().containsPoint( probe.locationProperty.get() );
    };

    thisNode.isInSolution = function() {
      return isInNode( solutionNode );
    };

    thisNode.isInWater = function() {
      return isInNode( waterFluidNode );
    };

    thisNode.isInDrainFluid = function() {
      return isInNode( drainFluidNode );
    };

    thisNode.isInDropperSolution = function() {
      return isInNode( dropperFluidNode );
    };
  }
Esempio n. 9
0
  QUnit.test( 'Setting left/right of node', function( assert ) {
    var node = new Path( Shape.rectangle( -20, -20, 50, 50 ), {
      scale: 2
    } );

    equalsApprox( assert, node.left, -40 );
    equalsApprox( assert, node.right, 60 );

    node.left = 10;
    equalsApprox( assert, node.left, 10 );
    equalsApprox( assert, node.right, 110 );

    node.right = 10;
    equalsApprox( assert, node.left, -90 );
    equalsApprox( assert, node.right, 10 );

    node.centerX = 5;
    equalsApprox( assert, node.centerX, 5 );
    equalsApprox( assert, node.left, -45 );
    equalsApprox( assert, node.right, 55 );
  } );
  /**
   *
   * @param {Property.<boolean>} showValues
   * @param {Property.<boolean>} visible
   * @param {number} verticalSpacingForCaptions
   * @param {boolean} showShowValuesCheckbox
   * @constructor
   */
  function ConcentrationBarChart( showValues, visible, verticalSpacingForCaptions, showShowValuesCheckbox ) {
    var self = this;
    Node.call( self );

    //@protected Background for the bar chart
    self.background = new Path( Shape.rectangle( 0, 0, 180, 170 + verticalSpacingForCaptions ), {
      fill: SugarAndSaltSolutionsConstants.WATER_COLOR//Background for the bar chart
    } );
    self.addChild( self.background );

    self.barChartContentNode = new Node();
    self.addChild( self.barChartContentNode );

    //The x-axis, the baseline for the bars
    //@protected
    self.abscissaY = self.background.bounds.getHeight() - 60 - verticalSpacingForCaptions;
    self.barChartContentNode.addChild( new Path( Shape.lineSegment( 0, self.abscissaY,
      self.background.bounds.getWidth(), self.abscissaY ), {
      lineWidth: 1,
      stroke: Color.BLACK
    } ) );

    //Add a checkbox that lets the user toggle on and off whether actual values are shown
    //It is only shown in the first tab, since values are suppressed in the Micro tab
    if ( showShowValuesCheckbox ) {
      var showValuesCheckbox = new Checkbox( new Text( showValuesString, { font: SugarAndSaltSolutionsConstants.CONTROL_FONT } ),
        showValues, {
          boxWidth: 20
        } );
      self.barChartContentNode.addChild( showValuesCheckbox );
      showValuesCheckbox.x = self.bounds.getWidth() / 2 - showValuesCheckbox.bounds.width / 2;
      showValuesCheckbox.y = self.getHeight() - showValuesCheckbox.bounds.getHeight() - INSET;
    }

    //Only show this bar chart if the user has opted to do so
    visible.linkAttribute( self, 'visible' );
  }
Esempio n. 11
0
  /**
   * RadioButtonGroup constructor.
   *
   * @param {Property} property
   * @param {Array} contentArray an array of objects that have two keys each: value and node the node key holds a
   * scenery Node that is the content for a given radio button. the value key should hold the value that the property
   * takes on for the corresponding node to be selected. Optionally, these objects can have an attribute 'label', which
   * is a {Node} used to label the button. You can also pass some specific a11y options
   * (labelContent/descriptionContent) through, see "new RadioButtonGroupMember" construction.
   * @param {Object} [options]
   * @constructor
   */
  function RadioButtonGroup( property, contentArray, options ) {
    options = _.extend( {
      tandem: Tandem.required,

      // a11y
      tagName: 'ul',
      labelTagName: 'h3',
      ariaRole: 'radiogroup',
      groupFocusHighlight: true
    }, options );

    // increment instance count
    instanceCount++;

    assert && assert( !options.hasOwnProperty( 'children' ), 'Cannot pass in children to a RadioButtonGroup, ' +
                                                             'create siblings in the parent node instead' );

    // make sure every object in the content array has properties 'node' and 'value'
    assert && assert( _.every( contentArray, function( obj ) {
      return obj.hasOwnProperty( 'node' ) && obj.hasOwnProperty( 'value' );
    } ), 'contentArray must be an array of objects with properties "node" and "value"' );

    var i; // for loops

    // make sure that each value passed into the contentArray is unique
    var uniqueValues = [];
    for ( i = 0; i < contentArray.length; i++ ) {
      if ( uniqueValues.indexOf( contentArray[ i ].value ) < 0 ) {
        uniqueValues.push( contentArray[ i ].value );
      }
      else {
        throw new Error( 'Duplicate value: "' + contentArray[ i ].value + '" passed into RadioButtonGroup.js' );
      }
    }

    // make sure that the property passed in currently has a value from the contentArray
    if ( uniqueValues.indexOf( property.get() ) === -1 ) {
      throw new Error( 'The property passed in to RadioButtonGroup has an illegal value "' + property.get() +
                       '" that is not present in the contentArray' );
    }

    var defaultOptions = {

      // LayoutBox options (super class of RadioButtonGroup)
      spacing: 10,
      orientation: 'vertical',

      enabledProperty: new Property( true ), // whether or not the set of radio buttons as a whole is enabled

      // The fill for the rectangle behind the radio buttons.  Default color is bluish color, as in the other button library.
      baseColor: ColorConstants.LIGHT_BLUE,
      disabledBaseColor: ColorConstants.LIGHT_GRAY,

      // Opacity can be set separately for the buttons and button content.
      selectedButtonOpacity: 1,
      deselectedButtonOpacity: 0.6,
      selectedContentOpacity: 1,
      deselectedContentOpacity: 0.6,
      overButtonOpacity: 0.8,
      overContentOpacity: 0.8,

      selectedStroke: 'black',
      deselectedStroke: new Color( 50, 50, 50 ),
      selectedLineWidth: 1.5,
      deselectedLineWidth: 1,

      // The following options specify highlight behavior overrides, leave as null to get the default behavior
      // Note that highlighting applies only to deselected buttons
      overFill: null,
      overStroke: null,
      overLineWidth: null,

      // These margins are *within* each button
      buttonContentXMargin: 5,
      buttonContentYMargin: 5,

      // alignment of the content nodes *within* each button
      buttonContentXAlign: 'center', // {string} see BUTTON_CONTENT_X_ALIGN_VALUES
      buttonContentYAlign: 'center', // {string} see BUTTON_CONTENT_Y_ALIGN_VALUES

      // TouchArea expansion
      touchAreaXDilation: 0,
      touchAreaYDilation: 0,

      // MouseArea expansion
      mouseAreaXDilation: 0,
      mouseAreaYDilation: 0,

      //The radius for each button
      cornerRadius: 4,

      // How far from the button the text label is (only applies if labels are passed in)
      labelSpacing: 0,

      // Which side of the button the label will appear, options are 'top', 'bottom', 'left', 'right'
      // (only applies if labels are passed in)
      labelAlign: 'bottom',

      // The default appearances use the color values specified above, but other appearances could be specified for more
      // customized behavior.  Generally setting the color values above should be enough to specify the desired look.
      buttonAppearanceStrategy: RadioButtonGroupAppearance.defaultRadioButtonsAppearance,
      contentAppearanceStrategy: RadioButtonGroupAppearance.contentAppearanceStrategy,

      // a11y - focus highlight expansion
      a11yHighlightXDilation: 0,
      a11yHighlightYDilation: 0
    };

    options = _.extend( _.clone( defaultOptions ), options );

    assert && assert( _.includes( BUTTON_CONTENT_X_ALIGN_VALUES, options.buttonContentXAlign ),
      'invalid buttonContentXAlign: ' + options.buttonContentXAlign );
    assert && assert( _.includes( BUTTON_CONTENT_Y_ALIGN_VALUES, options.buttonContentYAlign ),
      'invalid buttonContentYAlign: ' + options.buttonContentYAlign );

    // make a copy of the options to pass to individual buttons that includes all default options but not scenery options
    var buttonOptions = _.pick( options, _.keys( defaultOptions ) );

    // calculate the maximum width and height of the content so we can make all radio buttons the same size
    var widestContentWidth = _.maxBy( contentArray, function( content ) { return content.node.width; } ).node.width;
    var tallestContentHeight = _.maxBy( contentArray, function( content ) { return content.node.height; } ).node.height;

    // make sure all radio buttons are the same size and create the RadioButtons
    var buttons = [];
    var button;
    for ( i = 0; i < contentArray.length; i++ ) {
      var currentContent = contentArray[ i ];

      assert && assert( !currentContent.hasOwnProperty( 'phetioType' ), 'phetioType should be provided by ' +
                                                                        'the property passed to the ' +
                                                                        'RadioButtonGroup constructor' );

      assert && assert( !currentContent.tandem, 'content arrays should not have tandem instances, they should use ' +
                                                'tandemName instead' );

      var opts = _.extend( {
        content: currentContent.node,
        xMargin: options.buttonContentXMargin,
        yMargin: options.buttonContentYMargin,
        xAlign: options.buttonContentXAlign,
        yAlign: options.buttonContentYAlign,
        minWidth: widestContentWidth + 2 * options.buttonContentXMargin,
        minHeight: tallestContentHeight + 2 * options.buttonContentYMargin,
        phetioDocumentation: currentContent.phetioDocumentation || ''
      }, buttonOptions );

      // Pass through the tandem given the tandemName, but also support uninstrumented simulations
      if ( currentContent.tandemName ) {
        opts.tandem = options.tandem.createTandem( currentContent.tandemName );
      }

      // a11y create the label for the radio button
      if ( currentContent.labelContent ) {
        opts.labelContent = currentContent.labelContent;
      }

      // a11y create description for radio button
      // use if block to prevent empty 'p' tag being added when no option is present
      if ( currentContent.descriptionContent ) {
        opts.descriptionContent = currentContent.descriptionContent;
      }

      var radioButton = new RadioButtonGroupMember( property, currentContent.value, opts );

      // a11y - so the browser recognizes these buttons are in the same group, see instanceCount for more info
      radioButton.setAccessibleAttribute( 'name', CLASS_NAME + instanceCount );

      // ensure the buttons don't resize when selected vs unselected by adding a rectangle with the max size
      var maxLineWidth = Math.max( options.selectedLineWidth, options.deselectedLineWidth );
      var maxButtonWidth = maxLineWidth + widestContentWidth + options.buttonContentXMargin * 2;
      var maxButtonHeight = maxLineWidth + tallestContentHeight + options.buttonContentYMargin * 2;
      var boundingRect = new Rectangle( 0, 0, maxButtonWidth, maxButtonHeight, {
        fill: 'rgba(0,0,0,0)',
        center: radioButton.center
      } );
      radioButton.addChild( boundingRect );

      // default bounds for focus highlight, will include label if one exists
      var defaultHighlightBounds = null;

      // if a label is given, the button becomes a LayoutBox with the label and button
      if ( currentContent.label ) {
        var label = currentContent.label;
        var labelOrientation = ( options.labelAlign === 'bottom' || options.labelAlign === 'top' ) ? 'vertical' : 'horizontal';
        var labelChildren = ( options.labelAlign === 'left' || options.labelAlign === 'top' ) ? [ label, radioButton ] : [ radioButton, label ];
        button = new LayoutBox( {
          children: labelChildren,
          spacing: options.labelSpacing,
          orientation: labelOrientation
        } );

        var xDilation = options.touchAreaXDilation;
        var yDilation = options.touchAreaYDilation;

        // override the touch and mouse areas defined in RectangularButtonView
        // extra width is added to the SingleRadioButtons so they don't change size if the line width changes,
        // that is why lineWidth is subtracted from the width and height when calculating these new areas
        radioButton.touchArea = Shape.rectangle(
          -xDilation,
          -yDilation,
          button.width + 2 * xDilation - maxLineWidth,
          button.height + 2 * yDilation - maxLineWidth
        );

        xDilation = options.mouseAreaXDilation;
        yDilation = options.mouseAreaYDilation;
        radioButton.mouseArea = Shape.rectangle(
          -xDilation,
          -yDilation,
          button.width + 2 * xDilation - maxLineWidth,
          button.height + 2 * yDilation - maxLineWidth
        );

        // make sure the label mouse and touch areas don't block the expanded button touch and mouse areas
        label.pickable = false;

        // use the same content appearance strategy for the labels that is used for the button content
        options.contentAppearanceStrategy( label, radioButton.interactionStateProperty, options );

        // a11y - include label in focus highlight
        defaultHighlightBounds = radioButton.mouseArea.bounds.dilated( 5 );
      }
      else {
        button = radioButton;
        defaultHighlightBounds = button.bounds.dilated( FocusHighlightPath.getDilationCoefficient( button ) );
      }

      // a11y - set the focus highlight, dilated by the optional expansion values
      var highlightBounds = defaultHighlightBounds.dilatedX( opts.a11yHighlightXDilation ).dilatedY( opts.a11yHighlightYDilation );
      radioButton.setFocusHighlight( Shape.bounds( highlightBounds ) );

      buttons.push( button );
    }

    // @private
    this.enabledProperty = options.enabledProperty;

    // super call
    options.children = buttons;
    LayoutBox.call( this, options );
    var self = this;

    // a11y - this node's primary sibling is aria-labelledby its own label so the label content is read whenever
    // a member of the group receives focus
    this.addAriaLabelledbyAssociation( {
      thisElementName: AccessiblePeer.PRIMARY_SIBLING,
      otherNode: this,
      otherElementName: AccessiblePeer.LABEL_SIBLING
    } );

    // When the entire RadioButtonGroup gets disabled, gray them out and make them unpickable (and vice versa)
    var enabledListener = function( isEnabled ) {
      self.pickable = isEnabled;

      for ( i = 0; i < contentArray.length; i++ ) {
        if ( buttons[ i ] instanceof LayoutBox ) {
          for ( var j = 0; j < 2; j++ ) {
            buttons[ i ].children[ j ].enabled = isEnabled;
          }
        }
        else {
          buttons[ i ].enabled = isEnabled;
        }
      }
    };
    this.enabledProperty.link( enabledListener );

    // make the unselected buttons pickable and have a pointer cursor
    var propertyListener = function( value ) {
      if ( self.enabledProperty.get() ) {
        for ( i = 0; i < contentArray.length; i++ ) {
          if ( contentArray[ i ].value === value ) {
            buttons[ i ].pickable = false;
            buttons[ i ].cursor = null;
          }
          else {
            buttons[ i ].pickable = true;
            buttons[ i ].cursor = 'pointer';
          }
        }
      }
    };
    property.link( propertyListener );

    // @private - remove listeners from buttons and make eligible for garbage collection
    this.disposeRadioButtonGroup = function() {
      self.enabledProperty.unlink( enabledListener );
      property.unlink( propertyListener );

      // dispose all buttons
      for ( i = 0; i < contentArray.length; i++ ) {
        buttons[ i ].dispose();
      }
    };

    // a11y - register component for binder docs
    assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'sun', 'RadioButtonGroup', this );
  }
Esempio n. 12
0
  /**
   * @param {MolarityModel} model
   * @constructor
   */
  function MolarityView( model ) {

    var thisView = this;
    ScreenView.call( thisView, { layoutBounds: new Bounds2( 0, 0, 1100, 700 ) } );

    var valuesVisibleProperty = new Property( false );

    // beaker, with solution and precipitate inside of it
    var beakerNode = new BeakerNode( model.solution, MolarityModel.SOLUTION_VOLUME_RANGE.max, valuesVisibleProperty );
    var cylinderSize = beakerNode.getCylinderSize();
    var solutionNode = new SolutionNode( cylinderSize, beakerNode.getCylinderEndHeight(), model.solution, MolarityModel.SOLUTION_VOLUME_RANGE.max );
    var precipitateNode = new PrecipitateNode( model.solution, cylinderSize, beakerNode.getCylinderEndHeight(), model.maxPrecipitateAmount );
    var saturatedIndicator = new SaturatedIndicator( model.solution );

    // solute control
    var foregroundNode = new Node();
    var soluteComboBox = new SoluteComboBox( model.solutes, model.solution.soluteProperty, foregroundNode );

    // slider for controlling amount of solute
    var soluteAmountSlider = new VerticalSlider( soluteAmountString,
      StringUtils.format( pattern_parentheses_0text, molesString ),
      noneString, lotsString,
      new Dimension2( 5, cylinderSize.height ),
      model.solution.soluteAmountProperty,
      MolarityModel.SOLUTE_AMOUNT_RANGE,
      SOLUTE_AMOUNT_DECIMAL_PLACES,
      units_molesString,
      valuesVisibleProperty );

    // slider for controlling volume of solution, sized to match tick marks on the beaker
    var volumeSliderHeight = ( MolarityModel.SOLUTION_VOLUME_RANGE.getLength() / MolarityModel.SOLUTION_VOLUME_RANGE.max ) * cylinderSize.height;
    var solutionVolumeSlider = new VerticalSlider( solutionVolumeString,
      StringUtils.format( pattern_parentheses_0text, litersString ),
      lowString, fullString,
      new Dimension2( 5, volumeSliderHeight ),
      model.solution.volumeProperty,
      MolarityModel.SOLUTION_VOLUME_RANGE,
      VOLUME_DECIMAL_PLACES,
      units_litersString,
      valuesVisibleProperty );

    // concentration display
    var concentrationBarSize = new Dimension2( 40, cylinderSize.height + 50 );
    var concentrationDisplay = new ConcentrationDisplay( model.solution, MolarityModel.CONCENTRATION_DISPLAY_RANGE, valuesVisibleProperty, concentrationBarSize );

    // Show Values check box
    var showValuesCheckBox = CheckBox.createTextCheckBox( showValuesString, { font: new PhetFont( 22 ) }, valuesVisibleProperty );
    showValuesCheckBox.touchArea = Shape.rectangle( showValuesCheckBox.left, showValuesCheckBox.top - 15, showValuesCheckBox.width, showValuesCheckBox.height + 30 );

    // Reset All button
    var resetAllButton = new ResetAllButton( {
      listener: function() {
        valuesVisibleProperty.reset();
        model.reset();
      },
      scale: 1.32
    } );

    // rendering order
    this.addChild( solutionNode );
    this.addChild( beakerNode );
    this.addChild( precipitateNode );
    this.addChild( saturatedIndicator );
    this.addChild( soluteAmountSlider );
    this.addChild( solutionVolumeSlider );
    this.addChild( concentrationDisplay );
    this.addChild( showValuesCheckBox );
    this.addChild( resetAllButton );
    this.addChild( soluteComboBox );
    this.addChild( foregroundNode );

    // layout for things that don't have a location in the model
    {
      soluteAmountSlider.left = 35;
      soluteAmountSlider.top = 65;
      // to the right of the Solute Amount slider
      solutionVolumeSlider.left = soluteAmountSlider.right + 20;
      solutionVolumeSlider.y = soluteAmountSlider.y;
      // to the right of the Solution Volume slider
      beakerNode.left = solutionVolumeSlider.right + 20;
      beakerNode.y = soluteAmountSlider.y;
      // same coordinate frame as beaker
      solutionNode.x = beakerNode.x;
      solutionNode.y = beakerNode.y;
      // same coordinate frame as beaker
      precipitateNode.x = beakerNode.x;
      precipitateNode.y = beakerNode.y;
      // centered below beaker
      soluteComboBox.centerX = beakerNode.centerX;
      soluteComboBox.top = beakerNode.bottom + 50;
      // toward bottom of the beaker
      var saturatedIndicatorVisible = saturatedIndicator.visible; // so we can layout an invisible node
      saturatedIndicator.visible = true;
      saturatedIndicator.centerX = beakerNode.x + ( cylinderSize.width / 2 );
      saturatedIndicator.bottom = beakerNode.bottom - ( 0.2 * cylinderSize.height );
      saturatedIndicator.visible = saturatedIndicatorVisible;
      // right of beaker
      concentrationDisplay.left = beakerNode.right + 50;
      concentrationDisplay.bottom = beakerNode.bottom;
      // left of combo box
      showValuesCheckBox.right = soluteComboBox.left - 50;
      showValuesCheckBox.centerY = soluteComboBox.centerY;
      // right of combo box
      resetAllButton.left = Math.max( soluteComboBox.right + 10, concentrationDisplay.centerX - ( resetAllButton.width / 2 ) );
      resetAllButton.centerY = soluteComboBox.centerY;
    }
  }
Esempio n. 13
0
    /**
     * @param {GQModel} model
     * @param {GQViewProperties} viewProperties
     * @param {Object} [options]
     */
    constructor( model, viewProperties, options ) {

      options = _.extend( {

        // prevent a parabola's vertex and equation from overlapping
        preventVertexAndEquationOverlap: true,

        // {Node[]}, other curves to be displayed (terms, directrix, axis of symmetry) rendered in the order provided
        otherCurves: DEFAULT_OTHER_CURVES,

        // {Node[]}, decorations (manipulators, roots,...) rendered in the order provided
        decorations: DEFAULT_DECORATIONS
      }, options );

      assert && assert( !options.tandem, 'GQGraphNode should not be instrumented' );

      super( options );

      // Cartesian coordinates graph
      const graphNode = new GraphNode( model.graph, model.modelViewTransform );

      // Interactive quadratic curve
      const interactiveQuadraticNode = new QuadraticNode(
        model.quadraticProperty,
        model.graph.xRange,
        model.graph.yRange,
        model.modelViewTransform,
        viewProperties.equationForm,
        viewProperties.equationsVisibleProperty, {
          lineWidth: GQConstants.INTERACTIVE_QUADRATIC_LINE_WIDTH,
          preventVertexAndEquationOverlap: options.preventVertexAndEquationOverlap
        } );

      // {QuadraticNode|null} the saved line
      let savedLineNode = null;

      // Parent for other lines, e.g. quadratic terms, directrix, axis of symmetry
      const otherCurvesLayer = new Node( { children: options.otherCurves } );

      // Parent for decorations, e.g. vertex, roots, manipulators
      const decorationsLayer = new Node( { children: options.decorations } );

      // All lines, clipped to the graph
      const allLinesParent = new Node( {
        clipArea: Shape.rectangle(
          model.graph.xRange.min,
          model.graph.yRange.min,
          model.graph.xRange.getLength(),
          model.graph.yRange.getLength()
        ).transformed( model.modelViewTransform.getMatrix() ),
        children: [ otherCurvesLayer, interactiveQuadraticNode ]
      } );

      // Everything that's on the graph
      const contentParent = new Node( {
        children: [ allLinesParent, decorationsLayer ]
      } );

      // rendering order
      this.addChild( graphNode );
      this.addChild( contentParent );

      // When the saved quadratic changes...
      model.savedQuadraticProperty.link( savedQuadratic => {

        // remove and dispose any previously-saved line
        if ( savedLineNode ) {
          allLinesParent.removeChild( savedLineNode );
          savedLineNode.dispose();
          savedLineNode = null;
        }

        if ( savedQuadratic ) {
          savedLineNode = new QuadraticNode(
            new Property( savedQuadratic ),
            model.graph.xRange,
            model.graph.yRange,
            model.modelViewTransform,
            viewProperties.equationForm,
            viewProperties.equationsVisibleProperty, {
              lineWidth: GQConstants.SAVED_QUADRATIC_LINE_WIDTH,
              preventVertexAndEquationOverlap: options.preventVertexAndEquationOverlap
            } );

          // Add it in the foreground, so the user can see it.
          // See https://github.com/phetsims/graphing-quadratics/issues/36
          allLinesParent.addChild( savedLineNode );
        }
      } );

      // When quadraticProperty changes, move saved line to background.
      // See https://github.com/phetsims/graphing-quadratics/issues/36
      model.quadraticProperty.link( quadratic => {
        savedLineNode && savedLineNode.moveToBack();
      } );

      // Show/hide the graph contents
      viewProperties.graphContentsVisibleProperty.link( visible => {
        contentParent.visible = visible;
        if ( !visible ) {
          decorationsLayer.interruptSubtreeInput(); // cancel interaction with graph contents
        }
      } );
    }
Esempio n. 14
0
  /**
   * RadioButtonGroup constructor.
   *
   * @param {Property} property
   * @param {Array} contentArray an array of objects that have two keys each: value and node the node key holds a
   * scenery Node that is the content for a given radio button. the value key should hold the value that the property
   * takes on for the corresponding node to be selected. Optionally, these objects can have an attribute 'label', which
   * is a {Node} used to label the button.
   * @param {Object} [options]
   * @constructor
   */
  function RadioButtonGroup( property, contentArray, options ) {
    options = options || {};
    assert && assert( !options.hasOwnProperty( 'children' ), 'Cannot pass in children to a RadioButtonGroup, ' +
                                                             'create siblings in the parent node instead' );

    // make sure every object in the content array has properties 'node' and 'value'
    assert && assert( _.every( contentArray, function( obj ) {
      return obj.hasOwnProperty( 'node' ) && obj.hasOwnProperty( 'value' );
    } ), 'contentArray must be an array of objects with properties "node" and "value"' );

    var i; // for loops

    // make sure that each value passed into the contentArray is unique
    var uniqueValues = [];
    for ( i = 0; i < contentArray.length; i++ ) {
      if ( uniqueValues.indexOf( contentArray[ i ].value ) < 0 ) {
        uniqueValues.push( contentArray[ i ].value );
      }
      else {
        throw new Error( 'Duplicate value: "' + contentArray[ i ].value + '" passed into RadioButtonGroup.js' );
      }
    }

    // make sure that the property passed in currently has a value from the contentArray
    if ( uniqueValues.indexOf( property.get() ) === -1 ) {
      throw new Error( 'The property passed in to RadioButtonGroup has an illegal value "' + property.get() +
                       '" that is not present in the contentArray' );
    }

    var defaultOptions = {

      // LayoutBox options (super class of RadioButtonGroup)
      spacing: 10,
      orientation: 'vertical',
      align: 'center',

      enabledProperty: new Property( true ), // whether or not the set of radio buttons as a whole is enabled

      // The fill for the rectangle behind the radio buttons.  Default color is bluish color, as in the other button library.
      baseColor: ColorConstants.LIGHT_BLUE,
      disabledBaseColor: ColorConstants.LIGHT_GRAY,

      // Opacity can be set separately for the buttons and button content.
      selectedButtonOpacity: 1,
      deselectedButtonOpacity: 0.6,
      selectedContentOpacity: 1,
      deselectedContentOpacity: 0.6,
      overButtonOpacity: 0.8,
      overContentOpacity: 0.8,

      selectedStroke: 'black',
      deselectedStroke: new Color( 50, 50, 50 ),
      selectedLineWidth: 1.5,
      deselectedLineWidth: 1,

      // The following options specify highlight behavior overrides, leave as null to get the default behavior
      // Note that highlighting applies only to deselected buttons
      overFill: null,
      overStroke: null,
      overLineWidth: null,

      // These margins are *within* each button
      buttonContentXMargin: 5,
      buttonContentYMargin: 5,

      // TouchArea expansion
      touchAreaXDilation: 0,
      touchAreaYDilation: 0,

      // MouseArea expansion
      mouseAreaXDilation: 0,
      mouseAreaYDilation: 0,

      //The radius for each button
      cornerRadius: 4,

      // How far from the button the text label is (only applies if labels are passed in)
      labelSpacing: 0,

      // Which side of the button the label will appear, options are 'top', 'bottom', 'left', 'right'
      // (only applies if labels are passed in)
      labelAlign: 'bottom',

      // The default appearances use the color values specified above, but other appearances could be specified for more
      // customized behavior.  Generally setting the color values above should be enough to specify the desired look.
      buttonAppearanceStrategy: RadioButtonGroupAppearance.defaultRadioButtonsAppearance,
      contentAppearanceStrategy: RadioButtonGroupAppearance.contentAppearanceStrategy,

      // optional accessibility description, which applies to the entire group of buttons
      accessibleLegendDescription: ''
    };

    options = _.extend( _.clone( defaultOptions ), options );

    // make a copy of the options to pass to individual buttons that includes all default options but not scenery options
    var buttonOptions = _.pick( options, _.keys( defaultOptions ) );

    // calculate the maximum width and height of the content so we can make all radio buttons the same size
    var maxWidth = _.max( contentArray, function( content ) { return content.node.width; } ).node.width;
    var maxHeight = _.max( contentArray, function( content ) { return content.node.height; } ).node.height;

    // make sure all radio buttons are the same size and create the RadioButtons
    var buttons = [];
    var button;
    for ( i = 0; i < contentArray.length; i++ ) {

      // each individual radio button will have a different margin to make sure they are all the same size
      var xMargin = ( ( maxWidth - contentArray[ i ].node.width ) / 2 ) + options.buttonContentXMargin;
      var yMargin = ( ( maxHeight - contentArray[ i ].node.height ) / 2 ) + options.buttonContentYMargin;

      var radioButton = new RadioButtonGroupMember( property, contentArray[ i ].value,
        _.extend( {
          content: contentArray[ i ].node,
          xMargin: xMargin,
          yMargin: yMargin,
          tandem: contentArray[ i ].tandem,
          accessibleLabel: contentArray[ i ].accessibleLabel
        }, buttonOptions ) );

      // ensure the buttons don't resize when selected vs unselected by adding a rectangle with the max size
      var maxLineWidth = Math.max( options.selectedLineWidth, options.deselectedLineWidth );
      var maxButtonWidth = maxLineWidth + contentArray[ i ].node.width + xMargin * 2;
      var maxButtonHeight = maxLineWidth + contentArray[ i ].node.height + yMargin * 2;
      var boundingRect = new Rectangle( 0, 0, maxButtonWidth, maxButtonHeight, {
        fill: 'rgba(0,0,0,0)',
        center: radioButton.center
      } );
      radioButton.addChild( boundingRect );

      // if a label is given, the button becomes a LayoutBox with the label and button
      if ( contentArray[ i ].label ) {
        var label = contentArray[ i ].label;
        var labelOrientation = ( options.labelAlign === 'bottom' || options.labelAlign === 'top' ) ? 'vertical' : 'horizontal';
        var labelChildren = ( options.labelAlign === 'left' || options.labelAlign === 'top' ) ? [ label, radioButton ] : [ radioButton, label ];
        button = new LayoutBox( {
          children: labelChildren,
          spacing: options.labelSpacing,
          orientation: labelOrientation
        } );

        var xDilation = options.touchAreaXDilation;
        var yDilation = options.touchAreaYDilation;

        // override the touch and mouse areas defined in RectangularButtonView
        // extra width is added to the SingleRadioButtons so they don't change size if the line width changes,
        // that is why lineWidth is subtracted from the width and height when calculating these new areas
        radioButton.touchArea = Shape.rectangle( -xDilation, -yDilation, button.width + 2 * xDilation - maxLineWidth, button.height + 2 * yDilation - maxLineWidth );

        xDilation = options.mouseAreaXDilation;
        yDilation = options.mouseAreaYDilation;
        radioButton.mouseArea = Shape.rectangle( -xDilation, -yDilation, button.width + 2 * xDilation - maxLineWidth, button.height + 2 * yDilation - maxLineWidth );

        // make sure the label mouse and touch areas don't block the expanded button touch and mouse areas
        label.pickable = false;

        // use the same content appearance strategy for the labels that is used for the button content
        options.contentAppearanceStrategy( label, radioButton.interactionStateProperty, options );
      }
      else {
        button = radioButton;
      }
      buttons.push( button );
    }

    // @private
    this.enabledProperty = options.enabledProperty;

    // super call
    options.children = buttons;
    LayoutBox.call( this, options );
    var thisNode = this;

    // When the entire RadioButtonGroup gets disabled, gray them out and make them unpickable (and vice versa)
    this.enabledProperty.link( function( isEnabled ) {
      thisNode.pickable = isEnabled;

      for ( i = 0; i < contentArray.length; i++ ) {
        if ( buttons[ i ] instanceof LayoutBox ) {
          for ( var j = 0; j < 2; j++ ) {
            buttons[ i ].children[ j ].enabled = isEnabled;
          }
        }
        else {
          buttons[ i ].enabled = isEnabled;
        }
      }
    } );

    // make the unselected buttons pickable and have a pointer cursor
    property.link( function( value ) {
      if ( thisNode.enabledProperty.get() ) {
        for ( i = 0; i < contentArray.length; i++ ) {
          if ( contentArray[ i ].value === value ) {
            buttons[ i ].pickable = false;
            buttons[ i ].cursor = null;
          }
          else {
            buttons[ i ].pickable = true;
            buttons[ i ].cursor = 'pointer';
          }
        }
      }
    } );

    // generate accessible peer for the parallel DOM
    this.accessibleContent = {
      createPeer: function( accessibleInstance ) {
        var trail = accessibleInstance.trail;
        var uniqueId = trail.getUniqueId();

        /*
         We want the element of the parallel DOM to look like
         <fieldset id="radio-button-group" role="radiogroup" aria-describedby="legend-id group-description">
           <legend>Translatable legend text</legend>
         ... (radio inputs defined in RadioButtonGroupMember)
         <p id="group-description">Translatable description of the entire group.</p>
         </fieldset>
         */

        // create the fieldset holding all radio buttons
        var domElement = document.createElement( 'fieldset' );
        domElement.id = 'radio-button-group-' + uniqueId;
        domElement.setAttribute( 'role', 'radiogroup' );

        var legendElement = document.createElement( 'legend' );
        legendElement.innerHTML = options.accessibleLegendDescription;

        domElement.appendChild( legendElement );

        return new AccessiblePeer( accessibleInstance, domElement );

      }
    };
  }
Esempio n. 15
0
  /**
   * @param {Node[]} items - items in the carousel
   * @param {Object} [options]
   * @constructor
   */
  function Carousel( items, options ) {

    var self = this;

    // Override defaults with specified options
    options = _.extend( {}, DEFAULT_OPTIONS, options );

    // Validate options
    assert && assert( _.includes( [ 'horizontal', 'vertical' ], options.orientation ), 'invalid orientation=' + options.orientation );
    Tandem.indicateUninstrumentedCode();

    // To improve readability
    var isHorizontal = ( options.orientation === 'horizontal' );

    // Dimensions of largest item
    var maxItemWidth = _.maxBy( items, function( item ) { return item.width; } ).width;
    var maxItemHeight = _.maxBy( items, function( item ) { return item.height; } ).height;

    // This quantity is used make some other computations independent of orientation.
    var maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight;

    // Options common to both buttons
    var buttonOptions = {
      xMargin: 5,
      yMargin: 5,
      cornerRadius: options.cornerRadius,
      baseColor: options.buttonColor,
      disabledBaseColor: options.fill, // same as carousel background
      stroke: options.buttonStroke,
      lineWidth: options.buttonLineWidth,
      minWidth: isHorizontal ? 0 : maxItemWidth + ( 2 * options.margin ), // fill the width of a vertical carousel
      minHeight: isHorizontal ? maxItemHeight + ( 2 * options.margin ) : 0, // fill the height of a horizontal carousel
      arrowSize: options.arrowSize,
      arrowStroke: options.arrowStroke,
      arrowLineWidth: options.arrowLineWidth,
      touchAreaXDilation: options.buttonTouchAreaXDilation,
      touchAreaYDilation: options.buttonTouchAreaYDilation,
      mouseAreaXDilation: options.buttonMouseAreaXDilation,
      mouseAreaYDilation: options.buttonMouseAreaYDilation
    };

    // Next/previous buttons
    var nextButton = new CarouselButton( _.extend( { arrowDirection: isHorizontal ? 'right' : 'down' }, buttonOptions ) );
    var previousButton = new CarouselButton( _.extend( { arrowDirection: isHorizontal ? 'left' : 'up' }, buttonOptions ) );

    // Computations related to layout of items
    var numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0;
    var scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing );
    var scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin );
    var scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength;
    var itemCenter = options.spacing + ( maxItemLength / 2 );

    // Options common to all separators
    var separatorOptions = {
      stroke: options.separatorColor,
      lineWidth: options.separatorLineWidth
    };

    // @private enables animation when scrolling between pages
    this._animationEnabled = options.animationEnabled;

    // All items, arranged in the proper orientation, with margins and spacing.
    // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom.
    // Translation of this node will be animated to give the effect of scrolling through the items.
    var scrollingNode = new Rectangle( 0, 0, scrollingWidth, scrollingHeight );
    items.forEach( function( item ) {

      // add the item
      if ( isHorizontal ) {
        item.centerX = itemCenter;
        item.centerY = options.margin + ( maxItemHeight / 2 );
      }
      else {
        item.centerX = options.margin + ( maxItemWidth / 2 );
        item.centerY = itemCenter;
      }
      scrollingNode.addChild( item );

      // center for the next item
      itemCenter += ( options.spacing + maxItemLength );

      // add optional separator
      if ( options.separatorsVisible ) {
        var separator;
        if ( isHorizontal ) {

          // vertical separator, to the left of the item
          separator = new VSeparator( scrollingHeight, _.extend( {
            centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing,
            centerY: item.centerY
          }, separatorOptions ) );
          scrollingNode.addChild( separator );

          // center for the next item
          itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 );
        }
        else {

          // horizontal separator, below the item
          separator = new HSeparator( scrollingWidth, _.extend( {
            centerX: item.centerX,
            centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing
          }, separatorOptions ) );
          scrollingNode.addChild( separator );

          // center for the next item
          itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 );
        }
      }
    } );

    // How much to translate scrollingNode each time a next/previous button is pressed
    var scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing );
    if ( options.separatorsVisible ) {
      scrollingDelta += ( options.itemsPerPage * options.spacing );
    }

    // Clipping window, to show one page at a time.
    // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible.
    var windowLength = ( scrollingDelta + options.spacing );
    if ( options.separatorsVisible ) {
      windowLength -= options.spacing;
    }
    var windowWidth = isHorizontal ? windowLength : scrollingNode.width;
    var windowHeight = isHorizontal ? scrollingNode.height : windowLength;
    var clipArea = isHorizontal ?
                   Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) :
                   Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing );
    var windowNode = new Node( {
      children: [ scrollingNode ],
      clipArea: clipArea
    } );

    // Background - displays the carousel's fill color
    var backgroundWidth = isHorizontal ? ( windowWidth + nextButton.width + previousButton.width ) : windowWidth;
    var backgroundHeight = isHorizontal ? windowHeight : ( windowHeight + nextButton.height + previousButton.height );
    var backgroundNode = new Rectangle( 0, 0, backgroundWidth, backgroundHeight, options.cornerRadius, options.cornerRadius, {
      fill: options.fill
    } );

    // Foreground - displays the carousel's outline, created as a separate node so that it can be placed on top of everything, for a clean look.
    var foregroundNode = new Rectangle( 0, 0, backgroundWidth, backgroundHeight, options.cornerRadius, options.cornerRadius, {
      stroke: options.stroke
    } );

    // Layout
    if ( isHorizontal ) {
      nextButton.centerY = previousButton.centerY = windowNode.centerY = backgroundNode.centerY;
      nextButton.right = backgroundNode.right;
      previousButton.left = backgroundNode.left;
      windowNode.centerX = backgroundNode.centerX;
    }
    else {
      nextButton.centerX = previousButton.centerX = windowNode.centerX = backgroundNode.centerX;
      nextButton.bottom = backgroundNode.bottom;
      previousButton.top = backgroundNode.top;
      windowNode.centerY = backgroundNode.centerY;
    }

    // Number of pages
    var numberOfPages = items.length / options.itemsPerPage;
    if ( !Util.isInteger( numberOfPages ) ) {
      numberOfPages = Math.floor( numberOfPages + 1 );
    }

    // Number of the page that is visible in the carousel.
    assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1,
      'defaultPageNumber is out of range: ' + options.defaultPageNumber );
    var pageNumberProperty = new Property( options.defaultPageNumber );

    // Change pages
    var scrollAnimation = null;

    function pageNumberListener( pageNumber ) {

      assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, 'pageNumber out of range: ' + pageNumber );

      // button state
      nextButton.enabled = pageNumber < ( numberOfPages - 1 );
      previousButton.enabled = pageNumber > 0;
      if ( options.hideDisabledButtons ) {
        nextButton.visible = nextButton.enabled;
        previousButton.visible = previousButton.enabled;
      }

      // stop any animation that's in progress
      scrollAnimation && scrollAnimation.stop();

      if ( self._animationEnabled ) {

        // options that are independent of orientation
        var animationOptions = {
          duration: options.animationDuration,
          stepEmitter: options.stepEmitter,
          easing: Easing.CUBIC_IN_OUT
        };

        // options that are specific to orientation
        if ( isHorizontal ) {
          animationOptions = _.extend( {
            getValue: function() { return scrollingNode.left; },
            setValue: function( value ) { scrollingNode.left = value; },
            to: -pageNumber * scrollingDelta
          }, animationOptions );
        }
        else {
          animationOptions = _.extend( {
            getValue: function() { return scrollingNode.top; },
            setValue: function( value ) { scrollingNode.top = value; },
            to: -pageNumber * scrollingDelta
          }, animationOptions );
        }

        // create and start the scroll animation
        scrollAnimation = new Animation( animationOptions );
        scrollAnimation.start();
      }
      else {

        // animation disabled, move immediate to new page
        if ( isHorizontal ) {
          scrollingNode.left = -pageNumber * scrollingDelta;
        }
        else {
          scrollingNode.top = -pageNumber * scrollingDelta;
        }
      }
    }

    pageNumberProperty.link( pageNumberListener );

    // Buttons modify the page number
    nextButton.addListener( function() {
      pageNumberProperty.set( pageNumberProperty.get() + 1 );
    } );
    previousButton.addListener( function() {
      pageNumberProperty.set( pageNumberProperty.get() - 1 );
    } );

    // fields
    this.items = items; // @private
    this.itemsPerPage = options.itemsPerPage; // @private
    this.numberOfPages = numberOfPages; // @public (read-only) {number} number of pages in the carousel
    this.pageNumberProperty = pageNumberProperty; // @public {Property<number>} page number that is currently visible

    options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ];

    this.disposeCarousel = function() {
      pageNumberProperty.unlink( pageNumberListener );
    };

    Node.call( this, options );
  }