/**
   * @param {BaseGameModel} model
   * @param {HTMLImageElement[][]} levelImages - grid of images for the level-selection buttons, ordered by level
   * @param {function[]} rewardFactoryFunctions - functions that create nodes for the game reward, ordered by level
   * @constructor
   */
  function BaseGameScreenView( model, levelImages, rewardFactoryFunctions ) {

    ScreenView.call( this, GLConstants.SCREEN_VIEW_OPTIONS );

    // sounds
    var audioPlayer = new GameAudioPlayer( model.soundEnabledProperty );

    // @private one parent node for each 'phase' of the game
    this.settingsNode = new SettingsNode( model, this.layoutBounds, levelImages );
    this.playNode = new PlayNode( model, this.layoutBounds, this.visibleBoundsProperty, audioPlayer );
    this.resultsNode = new ResultsNode( model, this.layoutBounds, audioPlayer, rewardFactoryFunctions );

    // rendering order
    this.addChild( this.resultsNode );
    this.addChild( this.playNode );
    this.addChild( this.settingsNode );

    // game 'phase' changes
    // unlink unnecessary because BaseGameScreenView exists for the lifetime of the sim.
    var self = this;
    model.gamePhaseProperty.link( function( gamePhase ) {
      self.settingsNode.visible = ( gamePhase === GamePhase.SETTINGS );
      self.playNode.visible = ( gamePhase === GamePhase.PLAY );
      self.resultsNode.visible = ( gamePhase === GamePhase.RESULTS );
    } );
  }
Example #2
0
  /**
   * @constructor
   */
  function DialogsScreenView() {

    ScreenView.call( this );

    // dialog will be created the first time the button is pressed, lazily because Dialog
    // requires sim bounds during Dialog construction
    var dialog = null;

    var modalDialogButton = new RectangularPushButton( {
      content: new Text( 'modal dialog', { font: BUTTON_FONT } ),
      listener: function() {
        if ( !dialog ) {
          dialog = createDialog( true );
        }
        dialog.show();
      },
      left: this.layoutBounds.left + 100,
      top: this.layoutBounds.top + 100
    } );
    this.addChild( modalDialogButton );

    // var nonModalDialogButton = new RectangularPushButton( {
    //   content: new Text( 'non-modal dialog', { font: BUTTON_FONT } ),
    //   listener: function() {
    //     createDialog( false ).show();
    //   },
    //   left: modalDialogButton.right + 20,
    //   top: modalDialogButton.top
    // } );
    // this.addChild( nonModalDialogButton );
  }
Example #3
0
  /**
   * @param {gameModel} model - Faraday's Law simulation model object
   * @constructor
   */
  function FaradaysLawView( model ) {
    ScreenView.call( this, {
      renderer: 'svg',
      layoutBounds: FaradaysLawConstants.LAYOUT_BOUNDS
    } );

    // coils
    var bottomCoilNode = new CoilNode( CoilTypeEnum.FOUR_COIL, {
      x: model.bottomCoil.position.x,
      y: model.bottomCoil.position.y
    } );

    var topCoilNode = new CoilNode( CoilTypeEnum.TWO_COIL, {
      x: model.topCoil.position.x,
      y: model.topCoil.position.y
    } );

    // aligner
    this.aligner = new Aligner( model, bottomCoilNode.endRelativePositions, topCoilNode.endRelativePositions );

    // voltmeter and bulb created
    var voltMeterNode = new VoltMeterNode( model.voltMeterModel.thetaProperty, {} );
    var bulbNode = new BulbNode( model.voltMeterModel.thetaProperty, {
      centerX: this.aligner.bulbPosition.x,
      centerY: this.aligner.bulbPosition.y
    } );

    // wires
    this.addChild( new CoilsWiresNode( this.aligner, model.showSecondCoilProperty ) );
    this.addChild( new VoltMeterWiresNode( this.aligner, voltMeterNode ) );

    // bulb added
    this.addChild( bulbNode );

    // coils added
    this.addChild( bottomCoilNode );
    this.addChild( topCoilNode );
    model.showSecondCoilProperty.linkAttribute( topCoilNode, 'visible' );

    // control panel
    this.addChild( new ControlPanelNode( model ) );

    // voltmeter added
    voltMeterNode.center = this.aligner.voltmeterPosition;
    this.addChild( voltMeterNode );

    // magnet
    this.addChild( new MagnetNodeWithField( model ) );

    // move coils to front
    bottomCoilNode.frontImage.detach();
    this.addChild( bottomCoilNode.frontImage );
    bottomCoilNode.frontImage.center = model.bottomCoil.position.plus( new Vector2( CoilNode.xOffset, 0 ) );

    topCoilNode.frontImage.detach();
    this.addChild( topCoilNode.frontImage );
    topCoilNode.frontImage.center = model.topCoil.position.plus( new Vector2( CoilNode.xOffset + CoilNode.twoOffset, 0 ) );
    model.showSecondCoilProperty.linkAttribute( topCoilNode.frontImage, 'visible' );
  }
  function AcidBaseSolutionsView( model ) {
    ScreenView.call( this, { renderer: 'svg' } );

    // add control panel
    this.addChild( new ControlPanel( model ).mutate( {right: this.layoutBounds.maxX} ) );

    // add workspace
    this.addChild( new Workspace( model ) );
  }
  /**
   * @param {BarMagnetModel} model
   * @constructor
   */
  function ExampleScreenView( model ) {

    var thisView = this;
    ScreenView.call( thisView );

    // model-view transform
    var modelViewTransform = ModelViewTransform2.createOffsetScaleMapping( new Vector2( thisView.layoutBounds.width / 2, thisView.layoutBounds.height / 2 ), 1 );

    thisView.addChild( new BarMagnetNode( model.barMagnet, modelViewTransform ) );
    thisView.addChild( new ControlPanel( model, { x: 50, y: 50 } ) );
  }
  /**
   * @param {ProjectileMotionModel} projectileMotionModel
   * @constructor
   */
  function ProjectileMotionScreenView( projectileMotionModel ) {

    ScreenView.call( this );

    // Reset All button
    var resetAllButton = new ResetAllButton( {
      listener: function() {
        projectileMotionModel.reset();
      },
      right: this.layoutBounds.maxX - 10,
      bottom: this.layoutBounds.maxY - 10
    } );
    this.addChild( resetAllButton );
  }
  /**
   * @param {BeersLawModel} model
   * @param {ModelViewTransform2} modelViewTransform
   * @param {Tandem} tandem
   * @constructor
   */
  function BeersLawScreenView( model, modelViewTransform, tandem ) {

    ScreenView.call( this, _.extend( {
        tandem: tandem
      }, BLLConstants.SCREEN_VIEW_OPTIONS ) );

    var lightNode = new LightNode( model.light, modelViewTransform, tandem.createTandem( 'lightNode' ) );
    var cuvetteNode = new CuvetteNode( model.cuvette, model.solutionProperty, modelViewTransform, BLLQueryParameters.cuvetteSnapInterval, tandem.createTandem( 'cuvetteNode' ) );
    var beamNode = new BeamNode( model.beam );
    var detectorNode = new ATDetectorNode( model.detector, model.light, modelViewTransform, tandem.createTandem( 'detectorNode' ) );
    var wavelengthControls = new WavelengthControls( model.solutionProperty, model.light, tandem.createTandem( 'wavelengthControls' ) );
    var rulerNode = new BLLRulerNode( model.ruler, modelViewTransform, tandem.createTandem( 'rulerNode' ) );
    var comboBoxListParent = new Node( { maxWidth: 500 } );
    var solutionControls = new SolutionControls( model.solutions, model.solutionProperty, comboBoxListParent, tandem.createTandem( 'solutionControls' ), { maxWidth: 575 } );

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

    // Rendering order
    this.addChild( wavelengthControls );
    this.addChild( resetAllButton );
    this.addChild( solutionControls );
    this.addChild( detectorNode );
    this.addChild( cuvetteNode );
    this.addChild( beamNode );
    this.addChild( lightNode );
    this.addChild( rulerNode );
    this.addChild( comboBoxListParent ); // last, so that combo box list is on top

    // Layout for things that don't have a location in the model.
    {
      // below the light
      wavelengthControls.left = lightNode.left;
      wavelengthControls.top = lightNode.bottom + 20;
      // below cuvette
      solutionControls.left = cuvetteNode.left;
      solutionControls.top = cuvetteNode.bottom + 60;
      // bottom right
      resetAllButton.right = this.layoutBounds.right - 30;
      resetAllButton.bottom = this.layoutBounds.bottom - 30;
    }
  }
  /**
   * Constructor for the ExampleScreenView, it creates the bar magnet node and control panel node.
   *
   * @param {ExampleModel} model - the model for the entire screen
   * @constructor
   */
  function ExampleScreenView( model ) {

    ScreenView.call( this, {
      layoutBounds: new Bounds2( 0, 0, 768, 504 )
    } );

    // model-view transform
    var center = new Vector2( this.layoutBounds.width / 2, this.layoutBounds.height / 2 );
    var modelViewTransform = ModelViewTransform2.createOffsetScaleMapping( center, 1 );

    this.addChild( new BarMagnetNode( model.barMagnet, modelViewTransform ) );
    this.addChild( new ControlPanel( model, {
      x: 50,
      y: 50
    } ) );
  }
  /**
   * @param {MakingTensExploreModel} makingTensCommonModel
   * @constructor
   */
  function MakingTensCommonView( makingTensModel, screenBounds, paperNumberNodeLayer ) {
    var self = this;
    ScreenView.call( this, { layoutBounds: screenBounds } );
    self.makingTensModel = makingTensModel;

    self.paperNumberLayerNode = new Node();
    paperNumberNodeLayer.addChild( self.paperNumberLayerNode );

    self.addUserCreatedNumberModel = makingTensModel.addUserCreatedNumberModel.bind( makingTensModel );
    self.combineNumbersIfApplicableCallback = this.combineNumbersIfApplicable.bind( this );

    function handlePaperNumberAdded( addedNumberModel ) {
      // Add a representation of the number.
      var paperNumberNode = new PaperNumberNode( addedNumberModel, self.addUserCreatedNumberModel, self.combineNumbersIfApplicableCallback );
      self.paperNumberLayerNode.addChild( paperNumberNode );

      // Move the shape to the front of this layer when grabbed by the user.
      addedNumberModel.userControlledProperty.link( function( userControlled ) {
        if ( userControlled ) {
          paperNumberNode.moveToFront();
        }
      } );

      makingTensModel.residentNumberModels.addItemRemovedListener( function removalListener( removedNumberModel ) {
        if ( removedNumberModel === addedNumberModel ) {
          self.paperNumberLayerNode.removeChild( paperNumberNode );
          makingTensModel.residentNumberModels.removeItemRemovedListener( removalListener );
        }
      } );
    }

    //Initial Number Node creation
    makingTensModel.residentNumberModels.forEach( handlePaperNumberAdded );

    // Observe new items
    makingTensModel.residentNumberModels.addItemAddedListener( handlePaperNumberAdded );
  }
  function ShapeshiftScreenView( model ) {
    phet.joist.display.backgroundColor = '#000';

    var self = this;

    window.screenView = this;

    var shapeshiftScreenView = this;
    this.model = model;

    var bounds = new Bounds2( 0, 0, 1024, 618 );
    ScreenView.call( this, { layoutBounds: bounds } );

    var showHomeScreen = function() {
      self.showNode( self.homeScreen );
    };

    this.arcadeNode = new ArcadeGameNode( model, this.layoutBounds, this.visibleBoundsProperty, showHomeScreen );
    this.adventureNode = new AdventureGameNode( model, this.layoutBounds, this.visibleBoundsProperty, showHomeScreen );
    this.freePlayNode = new FreeplayGameNode( model, this.layoutBounds, this.visibleBoundsProperty, showHomeScreen );

    this.preventFit = true;

    this.homeScreen = new HomeScreen( bounds, function() {
      self.showNode( self.arcadeNode );
    }, function() {
      self.showNode( self.adventureNode );
    }, function() {
      self.showNode( self.freePlayNode );
    } );
    self.showNode( self.homeScreen );

    var level = phet.chipper.getQueryParameter( 'level' );
    if ( level ) {
      this.showNode( this.adventureNode );
    }
  }
  /**
   * Constructor for the MotionView
   * @param {MotionModel} model model for the entire screen
   * @constructor
   */
  function MotionView( model ) {

    //Constants and fields
    this.model = model;

    //Call super constructor
    ScreenView.call( this, {renderer: 'svg'} );

    //Variables for this constructor, for convenience
    var motionView = this;
    var width = this.layoutBounds.width;
    var height = this.layoutBounds.height;

    //Constants
    var skyHeight = 362;
    var groundHeight = height - skyHeight;

    //Create the static background
    var skyGradient = new LinearGradient( 0, 0, 0, skyHeight ).addColorStop( 0, '#02ace4' ).addColorStop( 1, '#cfecfc' );
    this.sky = new Rectangle( -width, -skyHeight, width * 3, skyHeight * 2, {fill: skyGradient, pickable: false} );

    this.groundNode = new Rectangle( -width, skyHeight, width * 3, groundHeight * 2, {fill: '#c59a5b', pickable: false} );
    this.addChild( this.sky );
    this.addChild( this.groundNode );

    //Create the dynamic (moving) background
    this.addChild( new MovingBackgroundNode( model, this.layoutBounds.width / 2 ).mutate( { layerSplit: true } ) );

    //Add toolbox backgrounds for the objects
    var boxHeight = 180;
    this.addChild( new Rectangle( 10, height - boxHeight - 10, 300, boxHeight, 10, 10, {fill: '#e7e8e9', stroke: '#000000', lineWidth: 1, pickable: false} ) );
    this.addChild( new Rectangle( width - 10 - 300, height - boxHeight - 10, 300, boxHeight, 10, 10, { fill: '#e7e8e9', stroke: '#000000', lineWidth: 1, pickable: false} ) );

    //Add the pusher
    this.addChild( new PusherNode( model, this.layoutBounds.width ) );

    //Add the skateboard if on the 'motion' screen
    if ( model.skateboard ) {
      this.addChild( new Image( skateboardImage, {centerX: width / 2, y: 315 + 12, pickable: false} ) );
    }

    //Create the slider
    var disableText = function( node ) { return function( length ) {node.fill = length === 0 ? 'gray' : 'black';}; };
    var disableLeftProperty = new DerivedProperty( [model.fallenProperty, model.fallenDirectionProperty], function( fallen, fallenDirection ) {
      return fallen && fallenDirection === 'left';
    } );
    var disableRightProperty = new DerivedProperty( [model.fallenProperty, model.fallenDirectionProperty], function( fallen, fallenDirection ) {
      return fallen && fallenDirection === 'right';
    } );
    var sliderLabel = new Text( Strings.appliedForce, {font: new PhetFont( 22 ), centerX: width / 2, y: 430} );
    var slider = new HSlider( -500, 500, 300, model.appliedForceProperty, model.speedClassificationProperty, disableLeftProperty, disableRightProperty, {zeroOnRelease: true, centerX: width / 2 + 1, y: 535} ).addNormalTicks();

    this.addChild( sliderLabel );
    this.addChild( slider );

    //Position the units to the right of the text box.
    var readout = new Text( '???', {font: new PhetFont( 22 ), pickable: false} );
    readout.bottom = slider.top - 15;
    model.appliedForceProperty.link( function( appliedForce ) {
      readout.text = appliedForce.toFixed( 0 ) + ' ' + Strings.newtons; //TODO: i18n message format
      readout.centerX = width / 2;
    } );

    //Make 'Newtons Readout' stand out but not look like a text entry field
    this.textPanelNode = new Rectangle( 0, 0, readout.right - readout.left + 50, readout.height + 4, {fill: 'white', stroke: 'lightGray', centerX: width / 2, top: readout.y - readout.height + 2, pickable: false} );
    this.addChild( this.textPanelNode );
    this.addChild( readout );

    //Show left arrow button 'tweaker' to change the applied force in increments of 50
    var leftArrowButton = new ArrowButton( 'left', function() {
      model.appliedForce = Math.max( model.appliedForce - 50, -500 );
    }, {rectangleYMargin: 7, rectangleXMargin: 10, right: this.textPanelNode.left - 6, centerY: this.textPanelNode.centerY} );

    //Do not allow the user to apply a force that would take the object beyond its maximum velocity
    model.multilink( ['appliedForce', 'speedClassification', 'stackSize'], function( appliedForce, speedClassification, stackSize ) {leftArrowButton.setEnabled( stackSize > 0 && (speedClassification === 'LEFT_SPEED_EXCEEDED' ? false : appliedForce > -500 ) );} );
    this.addChild( leftArrowButton );

    //Show right arrow button 'tweaker' to change the applied force in increments of 50
    var rightArrowButton = new ArrowButton( 'right', function() {
      model.appliedForce = Math.min( model.appliedForce + 50, 500 );
    }, {rectangleYMargin: 7, rectangleXMargin: 10, left: this.textPanelNode.right + 6, centerY: this.textPanelNode.centerY} );

    //Do not allow the user to apply a force that would take the object beyond its maximum velocity
    model.multilink( ['appliedForce', 'speedClassification', 'stackSize'], function( appliedForce, speedClassification, stackSize ) { rightArrowButton.setEnabled( stackSize > 0 && (speedClassification === 'RIGHT_SPEED_EXCEEDED' ? false : appliedForce < 500 ) ); } );
    this.addChild( rightArrowButton );

    model.stack.lengthProperty.link( disableText( sliderLabel ) );
    model.stack.lengthProperty.link( disableText( readout ) );
    model.stack.lengthProperty.link( function( length ) { slider.enabled = length > 0; } );

    //Create the speedometer.  Specify the location after construction so we can set the 'top'
    var speedometerNode = new SpeedometerNode( model.velocityProperty, Strings.speed, MotionConstants.MAX_SPEED ).mutate( {x: width / 2, top: 2} );
    model.showSpeedProperty.linkAttribute( speedometerNode, 'visible' );

    //Move away from the stack if the stack getting too high.  No need to record this in the model since it will always be caused deterministically by the model.
    //Use Tween.JS to smoothly animate
    var itemsCentered = new Property( true );
    model.stack.lengthProperty.link( function() {

      //Move both the accelerometer and speedometer if the stack is getting too high, based on the height of items in the stack
      var stackHeightThreshold = 160;
      if ( motionView.stackHeight > stackHeightThreshold && itemsCentered.value ) {
        itemsCentered.value = false;
        new TWEEN.Tween( speedometerNode ).to( { centerX: 300}, 400 ).easing( TWEEN.Easing.Cubic.InOut ).start();
        if ( accelerometerNode ) {
          new TWEEN.Tween( accelerometerWithTickLabels ).to( { centerX: 300}, 400 ).easing( TWEEN.Easing.Cubic.InOut ).start();
        }
      }
      else if ( motionView.stackHeight <= stackHeightThreshold && !itemsCentered.value ) {
        itemsCentered.value = true;

        new TWEEN.Tween( speedometerNode ).to( { x: width / 2}, 400 ).easing( TWEEN.Easing.Cubic.InOut ).start();
        if ( accelerometerNode ) {
          new TWEEN.Tween( accelerometerWithTickLabels ).to( { centerX: width / 2}, 400 ).easing( TWEEN.Easing.Cubic.InOut ).start();
        }
      }
    } );
    this.addChild( speedometerNode );

    //Create and add the control panel
    var controlPanel = new MotionControlPanel( model );
    this.addChild( controlPanel );

    //Reset all button goes beneath the control panel
    var resetButton = new ResetAllButton( model.reset.bind( model ), {scale: 88 / 103} ).mutate( {centerX: controlPanel.centerX, top: controlPanel.bottom + 5} );
    this.addChild( resetButton );

    //Add the accelerometer, if on the final screen
    if ( model.accelerometer ) {

      var accelerometerNode = new AccelerometerNode( model.accelerationProperty );
      var labelAndAccelerometer = new VBox( {pickable: false, children: [new Text( 'Acceleration', {font: new PhetFont( 18 )} ), accelerometerNode]} );
      var tickLabel = function( label, tick ) {
        return new Text( label, {pickable: false, font: new PhetFont( 16 ), centerX: tick.centerX, top: tick.bottom + 27} );
      };
      var accelerometerWithTickLabels = new Node( {children: [labelAndAccelerometer, tickLabel( '-20', accelerometerNode.ticks[0] ),
        tickLabel( '0', accelerometerNode.ticks[2] ),
        tickLabel( '20', accelerometerNode.ticks[4] )], centerX: width / 2, y: 135, pickable: false} );
      model.showAccelerationProperty.linkAttribute( accelerometerWithTickLabels, 'visible' );

      this.addChild( accelerometerWithTickLabels );
    }

    //Iterate over the items in the model and create and add nodes for each one
    this.itemNodes = [];
    for ( var i = 0; i < model.items.length; i++ ) {
      var item = model.items[i];
      var Constructor = item.bucket ? WaterBucketNode : ItemNode;
      var itemNode = new Constructor( model, motionView, item,
        item.image,
        item.sittingImage || item.image,
        item.holdingImage || item.image,
        model.showMassesProperty );
      this.itemNodes.push( itemNode );

      //Provide a reference from the item model to its view so that view dimensions can be looked up easily
      item.view = itemNode;
      this.addChild( itemNode );
    }

    //Add the force arrows & associated readouts in front of the items
    var arrowScale = 0.3;
    this.sumArrow = new ReadoutArrow( Strings.sumOfForces, '#96c83c', this.layoutBounds.width / 2, 230, model.sumOfForcesProperty, model.showValuesProperty, {labelPosition: 'top', arrowScale: arrowScale} );
    model.multilink( ['showForce', 'showSumOfForces'], function( showForce, showSumOfForces ) {motionView.sumArrow.visible = showForce && showSumOfForces;} );
    this.sumOfForcesText = new Text( Strings.sumOfForcesEqualsZero, {pickable: false, font: new PhetFont( { size: 16, weight: 'bold' } ), centerX: width / 2, y: 200} );
    model.multilink( ['showForce', 'showSumOfForces', 'sumOfForces'], function( showForce, showSumOfForces, sumOfForces ) {motionView.sumOfForcesText.visible = showForce && showSumOfForces && !sumOfForces;} );
    this.appliedForceArrow = new ReadoutArrow( Strings.appliedForce, '#e66e23', this.layoutBounds.width / 2, 280, model.appliedForceProperty, model.showValuesProperty, {labelPosition: 'side', arrowScale: arrowScale} );
    this.frictionArrow = new ReadoutArrow( Strings.friction, '#e66e23', this.layoutBounds.width / 2, 280, model.frictionForceProperty, model.showValuesProperty, {labelPosition: 'side', arrowScale: arrowScale} );
    this.addChild( this.sumArrow );
    this.addChild( this.appliedForceArrow );
    this.addChild( this.frictionArrow );
    this.addChild( this.sumOfForcesText );

    //On the motion screens, when the 'Friction' label overlaps the force vector it should be displaced vertically
    model.multilink( ['appliedForce', 'frictionForce'], function( appliedForce, frictionForce ) {
      var sameDirection = (appliedForce < 0 && frictionForce < 0) || (appliedForce > 0 && frictionForce > 0);
      motionView.frictionArrow.labelPosition = sameDirection ? 'bottom' : 'side';
    } );

    model.showForceProperty.linkAttribute( this.appliedForceArrow, 'visible' );
    model.showForceProperty.linkAttribute( this.frictionArrow, 'visible' );

    //After the view is constructed, move one of the blocks to the top of the stack.
    model.viewInitialized( this );
  }
Example #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;
    }
  }
  /**
   * @constructor
   *
   * @param {ModelMoleculesModel} model the model for the entire screen
   */
  function MoleculeShapesScreenView( model ) {
    ScreenView.call( this, {
      layoutBounds: new Bounds2( 0, 0, 1024, 618 )
    } );

    var self = this;

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

    // our target for drags that don't hit other UI components
    this.backgroundEventTarget = Rectangle.bounds( this.layoutBounds, {} ); // @private
    this.addChild( this.backgroundEventTarget );

    // updated in layout
    this.activeScale = 1; // @private scale applied to interaction that isn't directly tied to screen coordinates (rotation)
    this.screenWidth = null; // @public
    this.screenHeight = null; // @public

    // main three.js Scene setup
    this.threeScene = new THREE.Scene(); // @private
    this.threeCamera = new THREE.PerspectiveCamera(); // @private will set the projection parameters on layout

    // @public {THREE.Renderer}
    this.threeRenderer = MoleculeShapesGlobals.useWebGLProperty.get() ? new THREE.WebGLRenderer( {
      antialias: true,
      preserveDrawingBuffer: phet.chipper.queryParameters.preserveDrawingBuffer
    } ) : new THREE.CanvasRenderer( {
      devicePixelRatio: 1 // hopefully helps performance a bit
    } );

    this.threeRenderer.setPixelRatio( window.devicePixelRatio || 1 );

    // @private {ContextLossFailureDialog|null} - dialog shown on context loss, constructed
    // lazily because Dialog requires sim bounds during construction
    this.contextLossDialog = null;

    // In the event of a context loss, we'll just show a dialog. See https://github.com/phetsims/molecule-shapes/issues/100
    if ( MoleculeShapesGlobals.useWebGLProperty.get() ) {
      this.threeRenderer.context.canvas.addEventListener( 'webglcontextlost', function( event ) {
        event.preventDefault();

        self.showContextLossDialog();

        if ( document.domain === 'phet.colorado.edu' ) {
          window._gaq && window._gaq.push( [ '_trackEvent', 'WebGL Context Loss', 'molecule-shapes ' + phet.joist.sim.version, document.URL ] );
        }
      } );
    }

    MoleculeShapesColorProfile.backgroundProperty.link( function( color ) {
      self.threeRenderer.setClearColor( color.toNumber(), 1 );
    } );

    MoleculeShapesScreenView.addLightsToScene( this.threeScene );

    this.threeCamera.position.copy( MoleculeShapesScreenView.cameraPosition ); // sets the camera's position

    // @private add the Canvas in with a DOM node that prevents Scenery from applying transformations on it
    this.domNode = new DOM( this.threeRenderer.domElement, {
      preventTransform: true, // Scenery 0.2 override for transformation
      invalidateDOM: function() { // don't do bounds detection, it's too expensive. We're not pickable anyways
        this.invalidateSelf( new Bounds2( 0, 0, 0, 0 ) );
      },
      pickable: false
    } );
    this.domNode.invalidateDOM();
    // Scenery 0.1 override for transformation
    this.domNode.updateCSSTransform = function() {};

    // support Scenery/Joist 0.2 screenshot (takes extra work to output)
    this.domNode.renderToCanvasSelf = function( wrapper ) {
      var canvas = null;

      var effectiveWidth = Math.ceil( self.screenWidth );
      var effectiveHeight = Math.ceil( self.screenHeight );

      // This WebGL workaround is so we can avoid the preserveDrawingBuffer setting that would impact performance.
      // We render to a framebuffer and extract the pixel data directly, since we can't create another renderer and
      // share the view (three.js constraint).
      if ( MoleculeShapesGlobals.useWebGLProperty.get() ) {

        // set up a framebuffer (target is three.js terminology) to render into
        var target = new THREE.WebGLRenderTarget( effectiveWidth, effectiveHeight, {
          minFilter: THREE.LinearFilter,
          magFilter: THREE.NearestFilter,
          format: THREE.RGBAFormat
        } );
        // render our screen content into the framebuffer
        self.render( target );

        // set up a buffer for pixel data, in the exact typed formats we will need
        var buffer = new window.ArrayBuffer( effectiveWidth * effectiveHeight * 4 );
        var imageDataBuffer = new window.Uint8ClampedArray( buffer );
        var pixels = new window.Uint8Array( buffer );

        // read the pixel data into the buffer
        var gl = self.threeRenderer.getContext();
        gl.readPixels( 0, 0, effectiveWidth, effectiveHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels );

        // create a Canvas with the correct size, and fill it with the pixel data
        canvas = document.createElement( 'canvas' );
        canvas.width = effectiveWidth;
        canvas.height = effectiveHeight;
        var tmpContext = canvas.getContext( '2d' );
        var imageData = tmpContext.createImageData( effectiveWidth, effectiveHeight );
        imageData.data.set( imageDataBuffer );
        tmpContext.putImageData( imageData, 0, 0 );
      }
      else {
        // If just falling back to Canvas, we can directly render out!
        canvas = self.threeRenderer.domElement;
      }

      var context = wrapper.context;
      context.save();

      // Take the pixel ratio into account, see https://github.com/phetsims/molecule-shapes/issues/149
      const inverse = 1 / ( window.devicePixelRatio || 1 );

      if ( MoleculeShapesGlobals.useWebGLProperty.get() ) {
        context.setTransform( 1, 0, 0, -1, 0, effectiveHeight ); // no need to take pixel scaling into account
      }
      else {
        context.setTransform( inverse, 0, 0, inverse, 0, 0 );
      }
      context.drawImage( canvas, 0, 0 );
      context.restore();
    };

    this.addChild( this.domNode );

    // overlay Scene for bond-angle labels (if WebGL)
    this.overlayScene = new THREE.Scene(); // @private
    this.overlayCamera = new THREE.OrthographicCamera(); // @private
    this.overlayCamera.position.z = 50; // @private

    this.addChild( new ResetAllButton( {
      right: this.layoutBounds.maxX - 10,
      bottom: this.layoutBounds.maxY - 10,
      listener: function() {
        model.reset();
      }
    } ) );

    this.addChild( new GeometryNamePanel( model, {
      left: this.layoutBounds.minX + 10,
      bottom: this.layoutBounds.maxY - 10
    } ) );

    // we only want to support dragging particles OR rotating the molecule (not both) at the same time
    var draggedParticleCount = 0;
    var isRotating = false;

    var multiDragListener = {
      down: function( event, trail ) {
        if ( !event.canStartPress() ) { return; }

        // if we are already rotating the entire molecule, no more drags can be handled
        if ( isRotating ) {
          return;
        }

        var dragMode = null;
        var draggedParticle = null;

        var pair = self.getElectronPairUnderPointer( event.pointer, !( event.pointer instanceof Mouse ) );
        if ( pair && !pair.userControlledProperty.get() ) {
          // we start dragging that pair group with this pointer, moving it along the sphere where it can exist
          dragMode = 'pairExistingSpherical';
          draggedParticle = pair;
          pair.userControlledProperty.set( true );
          draggedParticleCount++;
        }
        else if ( draggedParticleCount === 0 ) { // we don't want to rotate while we are dragging any particles
          // we rotate the entire molecule with this pointer
          dragMode = 'modelRotate';
          isRotating = true;
        }
        else {
          // can't drag the pair OR rotate the molecule
          return;
        }

        var lastGlobalPoint = event.pointer.point.copy();

        event.pointer.cursor = 'pointer';
        event.pointer.addInputListener( {
          // end drag on either up or cancel (not supporting full cancel behavior)
          up: function( event, trail ) {
            this.endDrag( event, trail );
          },
          cancel: function( event, trail ) {
            this.endDrag( event, trail );
          },

          move: function( event, trail ) {
            if ( dragMode === 'modelRotate' ) {
              var delta = event.pointer.point.minus( lastGlobalPoint );
              lastGlobalPoint.set( event.pointer.point );

              var scale = 0.007 / self.activeScale; // tuned constant for acceptable drag motion
              var newQuaternion = new THREE.Quaternion().setFromEuler( new THREE.Euler( delta.y * scale, delta.x * scale, 0 ) );
              newQuaternion.multiply( model.moleculeQuaternionProperty.get() );
              model.moleculeQuaternionProperty.value = newQuaternion;
            }
            else if ( dragMode === 'pairExistingSpherical' ) {
              if ( _.includes( model.moleculeProperty.get().groups, draggedParticle ) ) {
                draggedParticle.dragToPosition( self.getSphericalMoleculePosition( event.pointer.point, draggedParticle ) );
              }
            }
          },

          // not a Scenery event
          endDrag: function( event, trail ) {
            if ( dragMode === 'pairExistingSpherical' ) {
              draggedParticle.userControlledProperty.set( false );
              draggedParticleCount--;
            }
            else if ( dragMode === 'modelRotate' ) {
              isRotating = false;
            }
            event.pointer.removeInputListener( this );
            event.pointer.cursor = null;
          }
        } );
      }
    };
    this.backgroundEventTarget.addInputListener( multiDragListener );

    // Consider updating the cursor even if we don't move? (only if we have mouse movement)? Current development
    // decision is to ignore this edge case in favor of performance.
    this.backgroundEventTarget.addInputListener( {
      mousemove: function( event ) {
        self.backgroundEventTarget.cursor = self.getElectronPairUnderPointer( event.pointer, false ) ? 'pointer' : null;
      }
    } );

    // update the molecule view's rotation when the model's rotation changes
    model.moleculeQuaternionProperty.link( function( quaternion ) {
      // moleculeView is created in the subtype (not yet). will handle initial rotation in addMoleculeView
      if ( self.moleculeView ) {
        self.moleculeView.quaternion.copy( quaternion );
        self.moleculeView.updateMatrix();
        self.moleculeView.updateMatrixWorld();
      }
    } );

    // @private - create a pool of angle labels of the desired type
    this.angleLabels = [];
    for ( var i = 0; i < 15; i++ ) {
      if ( MoleculeShapesGlobals.useWebGLProperty.get() ) {
        this.angleLabels[ i ] = new LabelWebGLView( this.threeRenderer );
        this.overlayScene.add( this.angleLabels[ i ] );
        this.angleLabels[ i ].unsetLabel();
      }
      else {
        this.angleLabels[ i ] = new LabelFallbackNode();
        this.addChild( this.angleLabels[ i ] );
      }
    }
  }
  /**
   * Main view of the flow sim.
   * @param {FlowModel} flowModel of the simulation
   * @constructor
   */
  function FlowView( flowModel ) {

    var flowView = this;
    ScreenView.call( this, { renderer: 'svg' } );

    // view co-ordinates (370,140) map to model origin (0,0) with inverted y-axis (y grows up in the model)
    var modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping(
      Vector2.ZERO,
      new Vector2( 370, 140 ),
      50 ); //1m = 50Px

    var groundY = modelViewTransform.modelToViewY( 0 );
    var backgroundNodeStartX = -5000;
    var backgroundNodeWidth = 10000;
    var skyExtensionHeight = 10000;
    var groundDepth = 10000;

    // add rectangle on top of the sky node to extend sky upwards. See https://github.com/phetsims/fluid-pressure-and-flow/issues/87
    this.addChild( new Rectangle( backgroundNodeStartX, -skyExtensionHeight, backgroundNodeWidth, skyExtensionHeight, { stroke: '#01ACE4', fill: '#01ACE4' } ) );

    // add sky node
    this.addChild( new SkyNode( backgroundNodeStartX, 0, backgroundNodeWidth, groundY, groundY ) );

    // add ground node with gradient
    var groundNode = new GroundNode( backgroundNodeStartX, groundY, backgroundNodeWidth, groundDepth, 400, { topColor: '#9D8B61', bottomColor: '#645A3C' } );
    this.addChild( groundNode );

    // add grass above the ground
    var grassPattern = new Pattern( grassImg ).setTransformMatrix( Matrix3.scale( 0.25 ) );
    var grassRectYOffset = 1;
    var grassRectHeight = 10;

    this.addChild( new Rectangle( backgroundNodeStartX, grassRectYOffset, backgroundNodeWidth, grassRectHeight, {
      fill: grassPattern,
      bottom: groundNode.top
    } ) );

    // tools control panel
    var toolsControlPanel = new ToolsControlPanel( flowModel, { right: this.layoutBounds.right - 7, top: 7 } );
    this.addChild( toolsControlPanel );

    // units control panel
    var unitsControlPanel = new UnitsControlPanel( flowModel.measureUnitsProperty, 50, { right: toolsControlPanel.left - 7, top: toolsControlPanel.top } );
    this.addChild( unitsControlPanel );

    var fluxMeterNode = new FluxMeterNode( flowModel, modelViewTransform, { stroke: 'blue' } );
    flowModel.isFluxMeterVisibleProperty.linkAttribute( fluxMeterNode.ellipse2, 'visible' );

    //adding pipe Node
    this.pipeNode = new PipeNode( flowModel, flowModel.pipe, modelViewTransform, this.layoutBounds );
    this.addChild( this.pipeNode );

    // add the back ellipse of the fluxMeter to the pipe node's pre-particle layer
    this.pipeNode.preParticleLayer.addChild( fluxMeterNode.ellipse2 );

    // now add the front part of the fluxMeter
    this.addChild( fluxMeterNode );

    // add the reset button
    var resetAllButton = new ResetAllButton( {
      listener: function() {
        flowModel.reset();
        flowView.pipeNode.reset();
      },
      radius: 18,
      bottom: this.layoutBounds.bottom - 7,
      right: this.layoutBounds.right - 13
    } );
    this.addChild( resetAllButton );

    // add the fluid density control slider
    var fluidDensityControlNode = new ControlSlider(
      flowModel.measureUnitsProperty,
      flowModel.fluidDensityProperty,
      flowModel.getFluidDensityString.bind( flowModel ),
      flowModel.fluidDensityRange,
      flowModel.fluidDensityControlExpandedProperty,
      {
        right: resetAllButton.left - 55,
        bottom: resetAllButton.bottom,
        title: fluidDensityString,
        ticks: [
          {
            title: waterString,
            value: flowModel.fluidDensity
          },
          {
            title: gasolineString,
            value: flowModel.fluidDensityRange.min
          },
          {
            title: honeyString,
            value: flowModel.fluidDensityRange.max
          }
        ],
        scale: 0.9,
        titleAlign: 'center'
      } );
    this.addChild( fluidDensityControlNode );

    // add the sensors panel
    var sensorPanel = new Rectangle( 0, 0, 167, 85, 10, 10, { stroke: 'gray', lineWidth: 1, fill: '#f2fa6a', right: unitsControlPanel.left - 4, top: toolsControlPanel.top } );
    this.addChild( sensorPanel );

    flowModel.isGridInjectorPressedProperty.link( function( isGridInjectorPressed ) {
      if ( isGridInjectorPressed ) {
        flowModel.injectGridParticles();
        flowView.pipeNode.gridInjectorNode.redButton.enabled = false;
      }
      else {
        flowView.pipeNode.gridInjectorNode.redButton.enabled = true;
      }
    } );

    // add play pause button and step button
    var stepButton = new StepButton( function() {
      flowModel.timer.step( 0.016 );
      flowModel.propagateParticles( 0.016 );
    }, flowModel.isPlayProperty, { radius: 12, stroke: 'black', fill: '#005566', right: fluidDensityControlNode.left - 82, bottom: this.layoutBounds.bottom - 14 } );

    this.addChild( stepButton );

    var playPauseButton = new PlayPauseButton( flowModel.isPlayProperty, { radius: 18, stroke: 'black', fill: '#005566', y: stepButton.centerY, right: stepButton.left - inset } );
    this.addChild( playPauseButton );

    // add sim speed controls
    var slowMotionRadioBox = new AquaRadioButton( flowModel.speedProperty, 'slow', new Text( slowMotionString, { font: new PhetFont( 12 ) } ), { radius: 8 } );
    var normalMotionRadioBox = new AquaRadioButton( flowModel.speedProperty, 'normal', new Text( normalString, { font: new PhetFont( 12 ) } ), { radius: 8 } );
    var speedControlMaxWidth = ( slowMotionRadioBox.width > normalMotionRadioBox.width ) ? slowMotionRadioBox.width : normalMotionRadioBox.width;
    slowMotionRadioBox.touchArea = new Bounds2( slowMotionRadioBox.localBounds.minX, slowMotionRadioBox.localBounds.minY,
        slowMotionRadioBox.localBounds.minX + speedControlMaxWidth, slowMotionRadioBox.localBounds.maxY );
    normalMotionRadioBox.touchArea = new Bounds2( normalMotionRadioBox.localBounds.minX, normalMotionRadioBox.localBounds.minY,
        normalMotionRadioBox.localBounds.minX + speedControlMaxWidth, normalMotionRadioBox.localBounds.maxY );

    var speedControl = new VBox( {
      align: 'left',
      spacing: 5,
      children: [ slowMotionRadioBox, normalMotionRadioBox ] } );
    this.addChild( speedControl.mutate( { right: playPauseButton.left - 8, bottom: playPauseButton.bottom } ) );

    // add flow rate panel
    var flowRateControlNode = new ControlSlider(
      flowModel.measureUnitsProperty,
      flowModel.pipe.flowRateProperty,
      flowModel.getFluidFlowRateString.bind( flowModel ),
      flowModel.flowRateRange,
      flowModel.flowRateControlExpandedProperty,
      {
        right: speedControl.left - 20,
        bottom: fluidDensityControlNode.bottom,
        title: flowRateString,
        ticks: [
          {
            title: 'Min',
            value: Constants.MIN_FLOW_RATE
          },
          {
            title: 'Max',
            value: Constants.MAX_FLOW_RATE
          }
        ],
        ticksVisible: false,
        titleAlign: 'center',
        scale: 0.9
      } );
    this.addChild( flowRateControlNode );

    // add speedometers within the sensor panel bounds
    _.each( flowModel.speedometers, function( velocitySensor ) {
      velocitySensor.positionProperty.storeInitialValue( new Vector2( sensorPanel.visibleBounds.centerX - 75, sensorPanel.visibleBounds.centerY - 30 ) );
      velocitySensor.positionProperty.reset();
      this.addChild( new VelocitySensorNode( modelViewTransform, velocitySensor, flowModel.measureUnitsProperty,
        [ flowModel.pipe.flowRateProperty, flowModel.pipe.frictionProperty ], flowModel.getVelocityAt.bind( flowModel ),
        sensorPanel.visibleBounds, this.layoutBounds, { scale: 0.9 } ) );
    }.bind( this ) );

    // add barometers within the sensor panel bounds
    _.each( flowModel.barometers, function( barometer ) {
      barometer.positionProperty.storeInitialValue( new Vector2( sensorPanel.visibleBounds.centerX + 50, sensorPanel.visibleBounds.centerY - 10 ) );
      barometer.reset();
      this.addChild( new BarometerNode( modelViewTransform, barometer, flowModel.measureUnitsProperty,
        [ flowModel.fluidDensityProperty, flowModel.pipe.flowRateProperty, flowModel.pipe.frictionProperty ],
        flowModel.getPressureAtCoords.bind( flowModel ), flowModel.getPressureString.bind( flowModel ),
        sensorPanel.visibleBounds, this.layoutBounds, { minPressure: Constants.MIN_PRESSURE, maxPressure: Constants.MAX_PRESSURE, scale: 0.9 } ) );
    }.bind( this ) );

    // add the rule node
    this.addChild( new FPAFRuler( flowModel.isRulerVisibleProperty, flowModel.rulerPositionProperty,
      flowModel.measureUnitsProperty, modelViewTransform, this.layoutBounds ) );

  }
Example #15
0
 function MemoryTestsView() {
   ScreenView.call( this );
 }
  /**
   * Constructor for the screen view of Molecules and Light.
   *
   * @param {PhotonAbsorptionModel} photonAbsorptionModel
   * @param {Tandem} tandem - support for exporting instances from the sim
   * @constructor
   */
  function MoleculesAndLightScreenView( photonAbsorptionModel, tandem ) {

    ScreenView.call( this, { layoutBounds: new Bounds2( 0, 0, 768, 504 ) } );

    var modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping(
      Vector2.ZERO,
      new Vector2( Math.round( INTERMEDIATE_RENDERING_SIZE.width * 0.55 ),
        Math.round( INTERMEDIATE_RENDERING_SIZE.height * 0.50 ) ),
      0.10 ); // Scale factor - Smaller number zooms out, bigger number zooms in.

    // Create the observation window.  This will hold all photons, molecules, and photonEmitters for this photon
    // absorption model.
    var observationWindow = new ObservationWindow( photonAbsorptionModel, modelViewTransform, tandem );
    this.addChild( observationWindow );

    // This rectangle hides photons that are outside the observation window.
    // TODO: This rectangle is a temporary workaround that replaces the clipping area in ObservationWindow because of a
    // Safari specific SVG bug caused by clipping.  See https://github.com/phetsims/molecules-and-light/issues/105 and
    // https://github.com/phetsims/scenery/issues/412.
    var clipRectangle = new Rectangle( observationWindow.bounds.copy().dilate( 4 * FRAME_LINE_WIDTH ),
      CORNER_RADIUS, CORNER_RADIUS, {
        stroke: '#C5D6E8',
        lineWidth: 8 * FRAME_LINE_WIDTH
      } );
    this.addChild( clipRectangle );

    // Create the window frame node that borders the observation window.
    var windowFrameNode = new WindowFrameNode( observationWindow, '#BED0E7', '#4070CE' );
    this.addChild( windowFrameNode );

    // Set positions of the observation window and window frame.
    observationWindow.translate( OBSERVATION_WINDOW_LOCATION );
    clipRectangle.translate( OBSERVATION_WINDOW_LOCATION );
    windowFrameNode.translate( OBSERVATION_WINDOW_LOCATION );

    // Create the control panel for photon emission frequency.
    var photonEmissionControlPanel = new QuadEmissionFrequencyControlPanel( photonAbsorptionModel, tandem );
    photonEmissionControlPanel.leftTop = ( new Vector2( OBSERVATION_WINDOW_LOCATION.x, 350 ) );

    // Create the molecule control panel
    var moleculeControlPanel = new MoleculeSelectionPanel( photonAbsorptionModel, tandem );
    moleculeControlPanel.leftTop = ( new Vector2( 530, windowFrameNode.top ) );

    // Add reset all button.
    var resetAllButton = new ResetAllButton( {
      listener: function() { photonAbsorptionModel.reset(); },
      bottom: this.layoutBounds.bottom - 15,
      right: this.layoutBounds.right - 15,
      radius: 18,
      tandem: tandem.createTandem( 'resetAllButton' )
    } );
    this.addChild( resetAllButton );

    // Add play/pause button.
    var playPauseButton = new PlayPauseButton( photonAbsorptionModel.playProperty, {
      bottom: moleculeControlPanel.bottom + 60,
      centerX: moleculeControlPanel.centerX - 25,
      radius: 23,
      tandem: tandem.createTandem( 'playPauseButton' )
    } );
    this.addChild( playPauseButton );

    // Add step button to manually step the animation.
    var stepButton = new StepButton( function() { photonAbsorptionModel.manualStep(); }, photonAbsorptionModel.playProperty, {
      centerY: playPauseButton.centerY,
      centerX: moleculeControlPanel.centerX + 25,
      radius: 15,
      tandem: tandem.createTandem( 'stepButton' )
    } );
    this.addChild( stepButton );

    // Window that displays the EM spectrum upon request.  Constructed once here so that time is not waisted
    // drawing a new spectrum window every time the user presses the 'Show Light Spectrum' button.
    var spectrumWindow = new SpectrumWindow( tandem.createTandem( 'spectrumWindow' ) );

    // Add the button for displaying the electromagnetic spectrum. Scale down the button content when it gets too
    // large.  This is done to support translations.  Max width of this button is the width of the molecule control
    // panel minus twice the default x margin of a rectangular push button.
    var buttonContent = new Text( buttonCaptionString, { font: new PhetFont( 18 ) } );
    if ( buttonContent.width > moleculeControlPanel.width - 16 ) {
      buttonContent.scale( (moleculeControlPanel.width - 16 ) / buttonContent.width );
    }
    var showSpectrumButton = new RectangularPushButton( {
      content: buttonContent,
      baseColor: 'rgb(98, 173, 205)',
      listener: function() {
        spectrumWindow.show();
      },
      tandem: tandem.createTandem( 'showLightSpectrumButton' )
    } );
    showSpectrumButton.center = ( new Vector2( moleculeControlPanel.centerX, photonEmissionControlPanel.centerY - 33 ) );
    this.addChild( showSpectrumButton );

    // Add the nodes in the order necessary for correct layering.
    this.addChild( photonEmissionControlPanel );
    this.addChild( moleculeControlPanel );
  }
Example #17
0
  /**
   * @param {CustomModel} model
   * @param {ModelViewTransform2} modelViewTransform
   * @constructor
   */
  function CustomView( model, modelViewTransform ) {

    var thisView = this;
    ScreenView.call( thisView, { renderer: 'svg' } );

    // view-specific properties
    var viewProperties = new PropertySet( {
      ratioVisible: false,
      moleculeCountVisible: false,
      pHMeterExpanded: true,
      graphExpanded: true
    } );

    // beaker
    var beakerNode = new BeakerNode( model.beaker, modelViewTransform );
    var solutionNode = new SolutionNode( model.solution, model.beaker, modelViewTransform );
    var volumeIndicatorNode = new VolumeIndicatorNode( model.solution.volumeProperty, model.beaker, modelViewTransform );

    // 'H3O+/OH- ratio' representation
    var ratioNode = new RatioNode( model.beaker, model.solution, modelViewTransform, { visible: viewProperties.ratioVisibleProperty.get() } );
    viewProperties.ratioVisibleProperty.linkAttribute( ratioNode, 'visible' );

    // 'molecule count' representation
    var moleculeCountNode = new MoleculeCountNode( model.solution );
    viewProperties.moleculeCountVisibleProperty.linkAttribute( moleculeCountNode, 'visible' );

    // beaker controls
    var beakerControls = new BeakerControls( viewProperties.ratioVisibleProperty, viewProperties.moleculeCountVisibleProperty );

    // graph
    var graphNode = new GraphNode( model.solution, viewProperties.graphExpandedProperty, {
      isInteractive: true,
      logScaleHeight: 565
    } );

    // pH meter
    var pHMeterTop = 15;
    var pHMeterNode = new PHMeterNode( model.solution, modelViewTransform.modelToViewY( model.beaker.location.y ) - pHMeterTop, viewProperties.pHMeterExpandedProperty,
      { attachProbe: 'right', isInteractive: true } );

    var resetAllButton = new ResetAllButton( {
      scale: 1.32,
      listener: function() {
        model.reset();
        viewProperties.reset();
        graphNode.reset();
      }
    } );

    // Parent for all nodes added to this screen
    var rootNode = new Node( { children: [
      // nodes are rendered in this order
      solutionNode,
      pHMeterNode,
      ratioNode,
      beakerNode,
      moleculeCountNode,
      volumeIndicatorNode,
      beakerControls,
      graphNode,
      resetAllButton
    ] } );
    thisView.addChild( rootNode );

    // Layout of nodes that don't have a location specified in the model
    pHMeterNode.left = beakerNode.left;
    pHMeterNode.top = pHMeterTop;
    moleculeCountNode.centerX = beakerNode.centerX;
    moleculeCountNode.bottom = beakerNode.bottom - 25;
    beakerControls.centerX = beakerNode.centerX;
    beakerControls.top = beakerNode.bottom + 10;
    graphNode.right = beakerNode.left - 70;
    graphNode.top = pHMeterNode.top;
    resetAllButton.right = this.layoutBounds.right - 40;
    resetAllButton.bottom = this.layoutBounds.bottom - 20;
  }
  /**
   * Constructor.
   *
   * @param {BAAGameModel} gameModel
   * @param {Tandem} tandem
   * @constructor
   */
  function BAAGameScreenView( gameModel, tandem ) {


    ScreenView.call( this, {
      layoutBounds: ShredConstants.LAYOUT_BOUNDS,
      tandem: tandem
    } );
    var self = this;

    // Add a root node where all of the game-related nodes will live.
    var rootNode = new Node();
    self.addChild( rootNode );

    var startGameLevelNode = new StartGameLevelNode(
      gameModel,
      this.layoutBounds,
      tandem.createTandem( 'startGameLevelNode' )
    );

    var scoreboard = new FiniteStatusBar(
      this.layoutBounds,
      this.visibleBoundsProperty,
      gameModel.scoreProperty,
      {
        challengeIndexProperty: gameModel.challengeIndexProperty,
        numberOfChallengesProperty: new Property( BAAGameModel.CHALLENGES_PER_LEVEL ),
        elapsedTimeProperty: gameModel.elapsedTimeProperty,
        timerEnabledProperty: gameModel.timerEnabledProperty,
        barFill: 'rgb( 49, 117, 202 )',
        textFill: 'white',
        xMargin: 20,
        dynamicAlignment: false,
        levelVisible: false,
        challengeNumberVisible: false,
        startOverButtonOptions: {
          font: new PhetFont( 20 ),
          textFill: 'black',
          baseColor: '#e5f3ff',
          xMargin: 6,
          yMargin: 5,
          listener: function() { gameModel.newGame(); }
        },
        tandem: tandem.createTandem( 'scoreboard' )
      }
    );

    scoreboard.centerX = this.layoutBounds.centerX;
    scoreboard.top = 0;
    var gameAudioPlayer = new GameAudioPlayer( gameModel.soundEnabledProperty );
    this.rewardNode = null;
    this.levelCompletedNode = null; // @private

    // Monitor the game state and update the view accordingly.
    gameModel.stateProperty.link( function( state, previousState ) {

      ( previousState && previousState.disposeEmitter ) && previousState.disposeEmitter.emit();

      if ( state === BAAGameState.CHOOSING_LEVEL ) {
        rootNode.removeAllChildren();
        rootNode.addChild( startGameLevelNode );
        if ( self.rewardNode !== null ) {
          self.rewardNode.dispose();
        }
        if ( self.levelCompletedNode !== null ) {
          self.levelCompletedNode.dispose();
        }
        self.rewardNode = null;
        self.levelCompletedNode = null;
      }
      else if ( state === BAAGameState.LEVEL_COMPLETED ) {
        rootNode.removeAllChildren();
        if ( gameModel.scoreProperty.get() === BAAGameModel.MAX_POINTS_PER_GAME_LEVEL || BAAQueryParameters.reward ) {

          // Perfect score, add the reward node.
          self.rewardNode = new BAARewardNode( tandem.createTandem( 'rewardNode' ) );
          rootNode.addChild( self.rewardNode );

          // Play the appropriate audio feedback
          gameAudioPlayer.gameOverPerfectScore();
        }
        else if ( gameModel.scoreProperty.get() > 0 ) {
          gameAudioPlayer.gameOverImperfectScore();
        }

        if ( gameModel.provideFeedbackProperty.get() ) {

          // Add the dialog node that indicates that the level has been completed.
          self.levelCompletedNode = new LevelCompletedNode(
            gameModel.levelProperty.get() + 1,
            gameModel.scoreProperty.get(),
            BAAGameModel.MAX_POINTS_PER_GAME_LEVEL,
            BAAGameModel.CHALLENGES_PER_LEVEL,
            gameModel.timerEnabledProperty.get(),
            gameModel.elapsedTimeProperty.get(),
            gameModel.bestTimes[ gameModel.levelProperty.get() ].value,
            gameModel.newBestTime,
            function() { gameModel.stateProperty.set( BAAGameState.CHOOSING_LEVEL ); }, {
              centerX: self.layoutBounds.width / 2,
              centerY: self.layoutBounds.height / 2,
              levelVisible: false,
              maxWidth: self.layoutBounds.width,
              tandem: tandem.createTandem( 'levelCompletedNode' )
            }
          );
          rootNode.addChild( self.levelCompletedNode );
        }
      }
      else if ( typeof( state.createView ) === 'function' ) {
        // Since we're not in the start or game-over states, we must be
        // presenting a challenge.
        rootNode.removeAllChildren();
        var challengeView = state.createView( self.layoutBounds, tandem.createTandem( state.tandem.tail + 'View' ) );
        state.disposeEmitter.addListener( function disposeListener() {
          challengeView.dispose();
          state.disposeEmitter.removeListener( disposeListener );
        } );
        rootNode.addChild( challengeView );
        rootNode.addChild( scoreboard );
      }
    } );
  }
  /**
   * @param {TugOfWarModel} model
   * @constructor
   */
  function TugOfWarView( model ) {

    ScreenView.call( this, {renderer: 'svg', layoutBounds: LAYOUT_BOUNDS} );

    //Fit to the window and render the initial scene
    var width = this.layoutBounds.width;
    var height = this.layoutBounds.height;

    var tugOfWarView = this;
    this.model = model;

    //Create the sky and ground.  Allow the sky and ground to go off the screen in case the window is larger than the sim aspect ratio
    var skyHeight = 376;
    var grassY = 368;
    var groundHeight = height - skyHeight;
    this.addChild( new Rectangle( -width, -skyHeight, width * 3, skyHeight * 2, {fill: new LinearGradient( 0, 0, 0, skyHeight ).addColorStop( 0, '#02ace4' ).addColorStop( 1, '#cfecfc' )} ) );
    this.addChild( new Rectangle( -width, skyHeight, width * 3, groundHeight * 3, { fill: '#c59a5b'} ) );

    //Show the grass.
    this.addChild( new Image( grassImage, {x: 13, y: grassY} ) );
    this.addChild( new Image( grassImage, {x: 13 - grassImage.width, y: grassY} ) );
    this.addChild( new Image( grassImage, {x: 13 + grassImage.width, y: grassY} ) );

    this.cartNode = new Image( cartImage, {y: 221} );

    //Black caret below the cart
    this.addChild( new Path( new Shape().moveTo( -10, 10 ).lineTo( 0, 0 ).lineTo( 10, 10 ), { stroke: '#000000', lineWidth: 3, x: this.layoutBounds.width / 2, y: grassY + 10} ) );

    //Add toolbox backgrounds for the pullers
    var toolboxHeight = 216;
    this.addChild( new Rectangle( 25, this.layoutBounds.height - toolboxHeight - 4, 324, toolboxHeight, 10, 10, {fill: '#e7e8e9', stroke: '#000000', lineWidth: 1} ) );
    this.addChild( new Rectangle( 630, this.layoutBounds.height - toolboxHeight - 4, 324, toolboxHeight, 10, 10, { fill: '#e7e8e9', stroke: '#000000', lineWidth: 1} ) );

    //Split into another canvas to speed up rendering
    this.addChild( new Node( {layerSplit: true} ) );

    //Create the arrow nodes
    var opacity = 0.8;
    this.sumArrow = new ReadoutArrow( sumOfForcesString, '#7dc673', this.layoutBounds.width / 2, 100, this.model.netForceProperty, this.model.showValuesProperty, {lineDash: [ 10, 5 ], labelPosition: 'top', opacity: opacity} );
    this.leftArrow = new ReadoutArrow( leftForceString, '#bf8b63', this.layoutBounds.width / 2, 200, this.model.leftForceProperty, this.model.showValuesProperty, {lineDash: [ 10, 5], labelPosition: 'side', opacity: opacity} );
    this.rightArrow = new ReadoutArrow( rightForceString, '#bf8b63', this.layoutBounds.width / 2, 200, this.model.rightForceProperty, this.model.showValuesProperty, {lineDash: [ 10, 5], labelPosition: 'side', opacity: opacity} );

    //Arrows should be dotted when the sim is paused, but solid after pressing 'go'
    this.model.runningProperty.link( function( running ) {
      [tugOfWarView.sumArrow, tugOfWarView.leftArrow, tugOfWarView.rightArrow].forEach( function( arrow ) {
        arrow.setArrowDash( running ? null : [ 10, 5 ] );
      } );
    } );

    this.model.showSumOfForcesProperty.linkAttribute( this.sumArrow, 'visible' );

    this.ropeNode = new Image( ropeImage, {x: 51, y: 273 } );

    model.knots.forEach( function( knot ) { tugOfWarView.addChild( new KnotHighlightNode( knot ) ); } );

    this.addChild( this.ropeNode );

    this.model.cart.xProperty.link( function( x ) {
      tugOfWarView.cartNode.x = x + 412;
      tugOfWarView.ropeNode.x = x + 51;
    } );

    this.addChild( this.cartNode );

    //Add the go button, but only if there is a puller attached
    var goPauseButton = new GoPauseButton( this.model, this.layoutBounds.width );
    var goPauseButtonContainer = new Node( {children: [goPauseButton]} );
    this.addChild( goPauseButtonContainer );

    //Return button
    this.addChild( new ReturnButton( model, {centerX: this.layoutBounds.centerX, top: goPauseButton.bottom + 5} ) );

    //Lookup a puller image given a puller instance and whether they are leaning or not.
    var getPullerImage = function( puller, leaning ) {
      var type = puller.type;
      var size = puller.size;

      //todo: compress with more ternary?
      return type === 'blue' && size === 'large' && !leaning ? pullFigureLargeBlue0Image :
             type === 'blue' && size === 'large' && leaning ? pullFigureLargeBlue3Image :
             type === 'blue' && size === 'medium' && !leaning ? pullFigureBlue0Image :
             type === 'blue' && size === 'medium' && leaning ? pullFigureBlue3Image :
             type === 'blue' && size === 'small' && !leaning ? pullFigureSmallBlue0Image :
             type === 'blue' && size === 'small' && leaning ? pullFigureSmallBlue3Image :
             type === 'red' && size === 'large' && !leaning ? pullFigureLargeRed0Image :
             type === 'red' && size === 'large' && leaning ? pullFigureLargeRed3Image :
             type === 'red' && size === 'medium' && !leaning ? pullFigureRed0Image :
             type === 'red' && size === 'medium' && leaning ? pullFigureRed3Image :
             type === 'red' && size === 'small' && !leaning ? pullFigureSmallRed0Image :
             type === 'red' && size === 'small' && leaning ? pullFigureSmallRed3Image :
             null;
    };

    var pullerLayer = new Node();
    this.addChild( pullerLayer );
    this.model.pullers.forEach( function( puller ) {
      pullerLayer.addChild( new PullerNode( puller, tugOfWarView.model, getPullerImage( puller, false ), getPullerImage( puller, true ) ) );
    } );

    //Add the arrow nodes after the pullers so they will appear in the front in z-ordering
    this.addChild( this.leftArrow );
    this.addChild( this.rightArrow );
    this.addChild( this.sumArrow );

    //Show the control panel
    this.addChild( new TugOfWarControlPanel( this.model ).mutate( {right: 981 - 5, top: 5} ) );

    //Show the flag node when pulling is complete
    var showFlagNode = function() { tugOfWarView.addChild( new FlagNode( model, tugOfWarView.layoutBounds.width / 2, 10 ) ); };
    model.stateProperty.link( function( state ) { if ( state === 'completed' ) { showFlagNode(); } } );

    //Accessibility for reading out the total force
    var textProperty = new Property( '' );
    model.numberPullersAttachedProperty.link( function() {
      textProperty.value = 'Left force: ' + Math.abs( model.getLeftForce() ) + ' Newtons, ' +
                           'Right force: ' + Math.abs( model.getRightForce() ) + ' Newtons, ' +
                           'Net Force: ' + Math.abs( model.getNetForce() ) + ' Newtons ' +
                           (model.getNetForce() === 0 ? '' : model.getNetForce() > 0 ? 'to the right' : 'to the left');
    } );
    this.addLiveRegion( textProperty );

    var golfClap = new Sound( golfClapSound );

    //Play audio golf clap when game completed
    model.stateProperty.link( function( state ) {
      if ( state === 'completed' && model.volumeOn ) {
        golfClap.play();
      }
    } );

    //Show 'Sum of Forces = 0' when showForces is selected but the force is zero
    this.sumOfForcesText = new Text( sumOfForcesEqualsZeroString, {font: new PhetFont( { size: 16, weight: 'bold' } ), centerX: width / 2, y: 53} );
    model.multilink( ['netForce', 'showSumOfForces'], function( netForce, showSumOfForces ) {tugOfWarView.sumOfForcesText.visible = !netForce && showSumOfForces;} );
    this.addChild( this.sumOfForcesText );
  }
Example #20
0
  var NUM_NUCLEON_LAYERS = 5; // This is based on max number of particles, may need adjustment if that changes.

  /**
   * @param {BuildAnAtomModel} model
   * @param {Tandem} tandem
   * @constructor
   */
  function AtomView( model, tandem ) {


    ScreenView.call( this, {
      layoutBounds: ShredConstants.LAYOUT_BOUNDS,
      tandem: tandem
    } );

    var self = this;
    this.model = model;
    this.resetFunctions = [];

    // @protected
    this.periodicTableAccordionBoxExpandedProperty = new BooleanProperty( true, {
      tandem: tandem.createTandem( 'periodicTableAccordionBoxExpandedProperty' )
    } );

    // Create the model-view transform.
    var modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping(
      Vector2.ZERO,
      new Vector2( self.layoutBounds.width * 0.3, self.layoutBounds.height * 0.45 ),
      1.0 );

    // Add the node that shows the textual labels, the electron shells, and the center X marker.
    var atomNode = new AtomNode( model.particleAtom, modelViewTransform, {
      showElementNameProperty: model.showElementNameProperty,
      showNeutralOrIonProperty: model.showNeutralOrIonProperty,
      showStableOrUnstableProperty: model.showStableOrUnstableProperty,
      electronShellDepictionProperty: model.electronShellDepictionProperty,
      tandem: tandem.createTandem( 'atomNode' )
    } );
    this.addChild( atomNode );

    // Add the bucket holes.  Done separately from the bucket front for layering.
    _.each( model.buckets, function( bucket ) {
      self.addChild( new BucketHole( bucket, modelViewTransform, {
        pickable: false,
        tandem: tandem.createTandem( bucket.sphereBucketTandem.tail + 'Hole' )
      } ) );
    } );

    // add the layer where the nucleons and electrons will go, this is added last so that it remains on top
    var nucleonElectronLayer = new Node( { tandem: tandem.createTandem( 'nucleonElectronLayer' ) } );

    // Add the layers where the nucleons will exist.
    var nucleonLayers = [];
    var nucleonLayersTandem = tandem.createGroupTandem( 'nucleonLayers' );
    _.times( NUM_NUCLEON_LAYERS, function() {
      var nucleonLayer = new Node( { tandem: nucleonLayersTandem.createNextTandem() } );
      nucleonLayers.push( nucleonLayer );
      nucleonElectronLayer.addChild( nucleonLayer );
    } );
    nucleonLayers.reverse(); // Set up the nucleon layers so that layer 0 is in front.

    // Add the layer where the electrons will exist.
    var electronLayer = new Node( { layerSplit: true, tandem: tandem.createTandem( 'electronLayer' ) } );
    nucleonElectronLayer.addChild( electronLayer );

    // Add the nucleon particle views.
    var nucleonsGroupTandem = tandem.createGroupTandem( 'nucleons' );
    var electronsGroupTandem = tandem.createGroupTandem( 'electrons' );

    // add the nucleons
    var particleDragBounds = modelViewTransform.viewToModelBounds( this.layoutBounds );
    model.nucleons.forEach( function( nucleon ) {
      nucleonLayers[ nucleon.zLayerProperty.get() ].addChild( new ParticleView( nucleon, modelViewTransform, {
        dragBounds: particleDragBounds,
        tandem: nucleonsGroupTandem.createNextTandem()
      } ) );

      // Add a listener that adjusts a nucleon's z-order layering.
      nucleon.zLayerProperty.link( function( zLayer ) {
        assert && assert(
          nucleonLayers.length > zLayer,
          'zLayer for nucleon exceeds number of layers, max number may need increasing.'
        );
        // Determine whether nucleon view is on the correct layer.
        var onCorrectLayer = false;
        nucleonLayers[ zLayer ].children.forEach( function( particleView ) {
          if ( particleView.particle === nucleon ) {
            onCorrectLayer = true;
          }
        } );

        if ( !onCorrectLayer ) {

          // Remove particle view from its current layer.
          var particleView = null;
          for ( var layerIndex = 0; layerIndex < nucleonLayers.length && particleView === null; layerIndex++ ) {
            for ( var childIndex = 0; childIndex < nucleonLayers[ layerIndex ].children.length; childIndex++ ) {
              if ( nucleonLayers[ layerIndex ].children[ childIndex ].particle === nucleon ) {
                particleView = nucleonLayers[ layerIndex ].children[ childIndex ];
                nucleonLayers[ layerIndex ].removeChildAt( childIndex );
                break;
              }
            }
          }

          // Add the particle view to its new layer.
          assert && assert( particleView !== null, 'Particle view not found during relayering' );
          nucleonLayers[ zLayer ].addChild( particleView );
        }
      } );
    } );

    // Add the electron particle views.
    model.electrons.forEach( function( electron ) {
      electronLayer.addChild( new ParticleView( electron, modelViewTransform, {
        dragBounds: particleDragBounds,
        tandem: electronsGroupTandem.createNextTandem()
      } ) );
    } );

    // When the electrons are represented as a cloud, the individual particles become invisible when added to the atom.
    var updateElectronVisibility = function() {
      electronLayer.getChildren().forEach( function( electronNode ) {
        electronNode.visible = model.electronShellDepictionProperty.get() === 'orbits' || !model.particleAtom.electrons.contains( electronNode.particle );
      } );
    };
    model.particleAtom.electrons.lengthProperty.link( updateElectronVisibility );
    model.electronShellDepictionProperty.link( updateElectronVisibility );

    // Add the front portion of the buckets. This is done separately from the bucket holes for layering purposes.
    var bucketFrontLayer = new Node( { tandem: tandem.createTandem( 'bucketFrontLayer' ) } );

    _.each( model.buckets, function( bucket ) {
      var bucketFront = new BucketFront( bucket, modelViewTransform, {
        tandem: tandem.createTandem( bucket.sphereBucketTandem.tail + 'Front' )
      } );
      bucketFrontLayer.addChild( bucketFront );
      bucketFront.addInputListener( new BucketDragHandler( bucket, bucketFront, modelViewTransform, {
        tandem: tandem.createTandem( bucket.sphereBucketTandem.tail + 'DragHandler' )
      } ) );
    } );

    // Add the particle count indicator.
    var particleCountDisplay = new ParticleCountDisplay( model.particleAtom, 13, 250, {
      tandem: tandem.createTandem( 'particleCountDisplay' )
    } );  // Width arbitrarily chosen.
    this.addChild( particleCountDisplay );

    // Add the periodic table display inside of an accordion box.
    var periodicTableAndSymbol = new PeriodicTableAndSymbol(
      model.particleAtom,
      tandem.createTandem( 'periodicTableAndSymbol' ),
      {
        pickable: false
      }
    );
    periodicTableAndSymbol.scale( 0.55 ); // Scale empirically determined to match layout in design doc.
    var periodicTableAccordionBoxTandem = tandem.createTandem( 'periodicTableAccordionBox' );
    this.periodicTableAccordionBox = new AccordionBox( periodicTableAndSymbol, {
      cornerRadius: 3,
      titleNode: new Text( elementString, {
        font: ShredConstants.ACCORDION_BOX_TITLE_FONT,
        maxWidth: ShredConstants.ACCORDION_BOX_TITLE_MAX_WIDTH,
        tandem: periodicTableAccordionBoxTandem.createTandem( 'title' )
      } ),
      fill: ShredConstants.DISPLAY_PANEL_BACKGROUND_COLOR,
      contentAlign: 'left',
      titleAlignX: 'left',
      buttonAlign: 'right',
      expandedProperty: this.periodicTableAccordionBoxExpandedProperty,
      expandCollapseButtonOptions: {
        touchAreaXDilation: 8,
        touchAreaYDilation: 8
      },

      // phet-io
      tandem: periodicTableAccordionBoxTandem,

      // a11y
      labelContent: elementString
    } );
    this.addChild( this.periodicTableAccordionBox );

    var labelVisibilityControlPanelTandem = tandem.createTandem( 'labelVisibilityControlPanel' );
    var labelVisibilityControlPanel = new Panel( new VerticalCheckboxGroup( [ {
      node: new Text( elementString, {
        font: LABEL_CONTROL_FONT,
        maxWidth: LABEL_CONTROL_MAX_WIDTH,
        tandem: labelVisibilityControlPanelTandem.createTandem( 'elementText' )
      } ),
      property: model.showElementNameProperty,
      tandem: labelVisibilityControlPanelTandem.createTandem( 'showElementNameCheckbox' )
    }, {
      node: new Text( neutralSlashIonString, {
        font: LABEL_CONTROL_FONT,
        maxWidth: LABEL_CONTROL_MAX_WIDTH,
        tandem: labelVisibilityControlPanelTandem.createTandem( 'neutralOrIonText' )
      } ),
      property: model.showNeutralOrIonProperty,
      tandem: labelVisibilityControlPanelTandem.createTandem( 'showNeutralOrIonCheckbox' )
    }, {
      node: new Text( stableSlashUnstableString, {
        font: LABEL_CONTROL_FONT,
        maxWidth: LABEL_CONTROL_MAX_WIDTH,
        tandem: labelVisibilityControlPanelTandem.createTandem( 'stableUnstableText' )
      } ),
      property: model.showStableOrUnstableProperty,
      tandem: labelVisibilityControlPanelTandem.createTandem( 'showStableOrUnstableCheckbox' )
    } ], {
      checkboxOptions: { boxWidth: 12 },
      spacing: 8,
      tandem: tandem.createTandem( 'labelVisibilityCheckboxGroup' )
    } ), {
      fill: 'rgb( 245, 245, 245 )',
      lineWidth: LABEL_CONTROL_LINE_WIDTH,
      xMargin: 7.5,
      cornerRadius: 5,
      resize: false,
      tandem: labelVisibilityControlPanelTandem
    } );
    var numDividerLines = 2;
    var dividerLineShape = new Shape().moveTo( 0, 0 ).lineTo( labelVisibilityControlPanel.width - 2 * LABEL_CONTROL_LINE_WIDTH, 0 );
    for ( var dividerLines = 0; dividerLines < numDividerLines; dividerLines++ ) {
      var dividerLine1 = new Path( dividerLineShape, {
        lineWidth: 1,
        stroke: 'gray',
        centerY: labelVisibilityControlPanel.height * ( dividerLines + 1 ) / ( numDividerLines + 1 ),
        x: LABEL_CONTROL_LINE_WIDTH / 2
      } );
      labelVisibilityControlPanel.addChild( dividerLine1 );
    }

    this.addChild( labelVisibilityControlPanel );
    var labelVisibilityControlPanelTitle = new Text( showString, {
      font: new PhetFont( { size: 16, weight: 'bold' } ),
      maxWidth: labelVisibilityControlPanel.width,
      tandem: tandem.createTandem( 'labelVisibilityControlPanelTitle' )
    } );
    this.addChild( labelVisibilityControlPanelTitle );

    // Add the radio buttons that control the electron representation in the atom.
    var radioButtonRadius = 6;
    var orbitsRadioButtonTandem = tandem.createTandem( 'orbitsRadioButton' );
    var orbitsRadioButton = new AquaRadioButton(
      model.electronShellDepictionProperty,
      'orbits',
      new Text( orbitsString, {
          font: ELECTRON_VIEW_CONTROL_FONT,
          maxWidth: ELECTRON_VIEW_CONTROL_MAX_WIDTH,
          tandem: orbitsRadioButtonTandem.createTandem( 'orbitsText' )
        }
      ),
      { radius: radioButtonRadius, tandem: orbitsRadioButtonTandem }
    );
    var cloudRadioButtonTandem = tandem.createTandem( 'cloudRadioButton' );
    var cloudRadioButton = new AquaRadioButton(
      model.electronShellDepictionProperty,
      'cloud',
      new Text( cloudString, {
        font: ELECTRON_VIEW_CONTROL_FONT,
        maxWidth: ELECTRON_VIEW_CONTROL_MAX_WIDTH,
        tandem: cloudRadioButtonTandem.createTandem( 'cloudText' )
      } ),
      { radius: radioButtonRadius, tandem: cloudRadioButtonTandem }
    );
    var electronViewButtonGroup = new Node( { tandem: tandem.createTandem( 'electronViewButtonGroup' ) } );
    electronViewButtonGroup.addChild( new Text( modelString, {
      font: new PhetFont( {
        size: 14,
        weight: 'bold'
      } ),
      maxWidth: ELECTRON_VIEW_CONTROL_MAX_WIDTH + 20,
      tandem: tandem.createTandem( 'electronViewButtonGroupLabel' )
    } ) );
    orbitsRadioButton.top = electronViewButtonGroup.bottom + 5;
    orbitsRadioButton.left = electronViewButtonGroup.left;
    electronViewButtonGroup.addChild( orbitsRadioButton );
    cloudRadioButton.top = electronViewButtonGroup.bottom + 5;
    cloudRadioButton.left = electronViewButtonGroup.left;
    electronViewButtonGroup.addChild( cloudRadioButton );
    this.addChild( electronViewButtonGroup );

    // Add the reset button.
    var resetAllButton = new ResetAllButton( {
      listener: function() {
        self.model.reset();
        self.reset();
      },
      right: this.layoutBounds.maxX - CONTROLS_INSET,
      bottom: this.layoutBounds.maxY - CONTROLS_INSET,
      radius: BAASharedConstants.RESET_BUTTON_RADIUS,
      tandem: tandem.createTandem( 'resetAllButton' )
    } );
    this.addChild( resetAllButton );

    // Do the layout.
    particleCountDisplay.top = CONTROLS_INSET;
    particleCountDisplay.left = CONTROLS_INSET;
    this.periodicTableAccordionBox.top = CONTROLS_INSET;
    this.periodicTableAccordionBox.right = this.layoutBounds.maxX - CONTROLS_INSET;
    labelVisibilityControlPanel.left = this.periodicTableAccordionBox.left;
    labelVisibilityControlPanel.bottom = this.layoutBounds.height - CONTROLS_INSET;
    labelVisibilityControlPanelTitle.bottom = labelVisibilityControlPanel.top;
    labelVisibilityControlPanelTitle.centerX = labelVisibilityControlPanel.centerX;
    electronViewButtonGroup.left = atomNode.right + 30;
    electronViewButtonGroup.bottom = atomNode.bottom + 5;

    // Any other objects added by class calling it will be added in this node for layering purposes
    this.controlPanelLayer = new Node( { tandem: tandem.createTandem( 'controlPanelLayer' ) } );
    this.addChild( this.controlPanelLayer );

    this.addChild( nucleonElectronLayer );
    this.addChild( bucketFrontLayer );
  }
  /**
   * @constructor
   */
  function LevelSelectionScreenView() {

    ScreenView.call( this );

    var scoreProperty = new Property( 0 );
    var bestTimeProperty = new Property( 0 );
    var bestTimeVisibleProperty = new BooleanProperty( true );

    // Various options for displaying score.
    var scoreDisplays = new VBox( {
      resize: false,
      spacing: 20,
      align: 'left',
      centerX: this.layoutBounds.centerX,
      top: this.layoutBounds.top + 20,
      children: [
        new ScoreDisplayStars( scoreProperty, { numberOfStars: NUM_STARS, perfectScore: SCORE_RANGE.max } ),
        new ScoreDisplayLabeledStars( scoreProperty, { numberOfStars: NUM_STARS, perfectScore: SCORE_RANGE.max } ),
        new ScoreDisplayNumberAndStar( scoreProperty ),
        new ScoreDisplayLabeledNumber( scoreProperty )
      ]
    } );
    this.addChild( scoreDisplays );

    // Level selection buttons
    var buttonIcon = new Rectangle( 0, 0, 100, 100, { fill: 'red', stroke: 'black' } );

    var buttonWithStars = new LevelSelectionButton( buttonIcon, scoreProperty, {
      scoreDisplayConstructor: ScoreDisplayStars,
      scoreDisplayOptions: {
        numberOfStars: NUM_STARS,
        perfectScore: SCORE_RANGE.max
      },
      listener: function() { console.log( 'level start' ); }
    } );

    var buttonWithTextAndStars = new LevelSelectionButton( buttonIcon, scoreProperty, {
      scoreDisplayConstructor: ScoreDisplayLabeledStars,
      scoreDisplayOptions: {
        numberOfStars: NUM_STARS,
        perfectScore: SCORE_RANGE.max
      },
      listener: function() { console.log( 'level start' ); }
    } );

    var buttonWithNumberAndStar = new LevelSelectionButton( buttonIcon, scoreProperty, {
      scoreDisplayConstructor: ScoreDisplayNumberAndStar,
      listener: function() { console.log( 'level start' ); }
    } );

    var buttonWithTextAndNumber = new LevelSelectionButton( buttonIcon, scoreProperty, {
      scoreDisplayConstructor: ScoreDisplayLabeledNumber,
      listener: function() { console.log( 'level start' ); },
      bestTimeProperty: bestTimeProperty,
      bestTimeVisibleProperty: bestTimeVisibleProperty
    } );

    var levelSelectionButtons = new HBox( {
      spacing: 20,
      align: 'top',
      centerX: this.layoutBounds.centerX,
      top: scoreDisplays.bottom + 60,
      children: [ buttonWithStars, buttonWithTextAndStars, buttonWithNumberAndStar, buttonWithTextAndNumber ]
    } );
    this.addChild( levelSelectionButtons );

    // Controls for Properties
    var scoreSlider = new HBox( {
      children: [
        new Text( 'Score: ', { font: new PhetFont( 20 ) } ),
        new HSlider( scoreProperty, SCORE_RANGE )
      ]
    } );

    var bestTimeSlider = new HBox( {
      children: [
        new Text( 'Best Time: ', { font: new PhetFont( 20 ) } ),
        new HSlider( bestTimeProperty, BEST_TIME_RANGE )
      ]
    } );

    var bestTimeVisibleCheckbox = new Checkbox(
      new Text( 'Best time visible', { font: new PhetFont( 20 ) } ),
      bestTimeVisibleProperty );

    var controls = new HBox( {
      resize: false,
      spacing: 30,
      centerX: this.layoutBounds.centerX,
      top: levelSelectionButtons.bottom + 60,
      children: [ scoreSlider, bestTimeSlider, bestTimeVisibleCheckbox ]
    } );
    this.addChild( controls );
  }
  /**
   * @param {EnergySkateParkBasicsModel} model
   * @param {Object} [options]
   * @constructor
   */
  function EnergySkateParkBasicsScreenView( model, options ) {

    var view = this;
    ScreenView.call( view, { layoutBounds: new Bounds2( 0, 0, 834, 504 ) } );

    var modelPoint = new Vector2( 0, 0 );
    // earth is 70px high in stage coordinates
    var viewPoint = new Vector2( this.layoutBounds.width / 2, this.layoutBounds.height - BackgroundNode.earthHeight );
    var scale = 50;
    var modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( modelPoint, viewPoint, scale );
    this.modelViewTransform = modelViewTransform;

    this.availableModelBoundsProperty = new Property();
    this.availableModelBoundsProperty.linkAttribute( model, 'availableModelBounds' );

    // The background
    this.backgroundNode = new BackgroundNode( this.layoutBounds );
    this.addChild( this.backgroundNode );

    this.gridNode = new GridNode( model.property( 'gridVisible' ), modelViewTransform );
    this.addChild( this.gridNode );

    var pieChartLegend = new PieChartLegend(
      model.skater,
      model.clearThermal.bind( model ),
      model.property( 'pieChartVisible' ), {
        tandem: options.tandem
      } );
    this.addChild( pieChartLegend );

    this.controlPanel = new EnergySkateParkBasicsControlPanel( model, {
      tandem: options.tandem,
      massSliderPhETIOID: options.massSliderPhETIOID
    } );
    this.addChild( this.controlPanel );
    this.controlPanel.right = this.layoutBounds.width - 5;
    this.controlPanel.top = 5;

    // For the playground screen, show attach/detach toggle buttons
    if ( model.draggableTracks ) {
      var property = model.draggableTracks ? new Property( true ) :
                     new DerivedProperty( [ model.property( 'scene' ) ], function( scene ) { return scene === 2; } );
      this.attachDetachToggleButtons = new AttachDetachToggleButtons( model.property( 'detachable' ), property, this.controlPanel.contentWidth, {
        top: this.controlPanel.bottom + 5,
        centerX: this.controlPanel.centerX
      } );
      this.addChild( this.attachDetachToggleButtons );
    }

    var containsAbove = function( bounds, x, y ) {
      return bounds.minX <= x && x <= bounds.maxX && y <= bounds.maxY;
    };

    // Determine if the skater is onscreen or offscreen for purposes of highlighting the 'return skater' button.
    // Don't check whether the skater is underground since that is a rare case (only if the user is actively dragging a
    // control point near y=0 and the track curves below) and the skater will pop up again soon, see the related
    // flickering problem in #206
    var onscreenProperty = new DerivedProperty( [ model.skater.positionProperty ], function( position ) {
      if ( !view.availableModelBounds ) {
        return true;
      }
      return view.availableModelBounds && containsAbove( view.availableModelBounds, position.x, position.y );
    } );

    var barGraphBackground = new BarGraphBackground( model.skater, model.property( 'barGraphVisible' ), model.clearThermal.bind( model ),
      { tandem: options.tandem } );
    this.addChild( barGraphBackground );

    if ( !model.draggableTracks ) {
      this.sceneSelectionPanel = new SceneSelectionPanel( model, this, modelViewTransform, {
        tandem: options.tandem
      } );// layout done in layout bounds
      this.addChild( this.sceneSelectionPanel );
    }

    // Put the pie chart legend to the right of the bar chart, see #60, #192
    pieChartLegend.mutate( { top: barGraphBackground.top, left: barGraphBackground.right + 8 } );

    var playProperty = new Property( !model.property( 'paused' ).value );
    model.property( 'paused' ).link( function( paused ) {
      playProperty.set( !paused );
    } );
    playProperty.link( function( playing ) {
      model.property( 'paused' ).set( !playing );
    } );
    var playPauseButton = new PlayPauseButton( playProperty, { phetioID: 'playPauseButton' } ).mutate( { scale: 0.6 } );

    // Make the Play/Pause button bigger when it is showing the pause button, see #298
    var pauseSizeIncreaseFactor = 1.35;
    playProperty.lazyLink( function( isPlaying ) {
      playPauseButton.scale( isPlaying ? ( 1 / pauseSizeIncreaseFactor ) : pauseSizeIncreaseFactor );
    } );

    var stepButton = new StepForwardButton( function() { model.manualStep(); }, playProperty, {
      phetioID: options.tandem.createTandem( 'stepButton' )
    } );

    // Make the step button the same size as the pause button.
    stepButton.mutate( { scale: playPauseButton.height / stepButton.height } );
    model.property( 'paused' ).linkAttribute( stepButton, 'enabled' );

    this.addChild( playPauseButton.mutate( {
      centerX: this.layoutBounds.centerX,
      bottom: this.layoutBounds.maxY - 15
    } ) );
    this.addChild( stepButton.mutate( { left: playPauseButton.right + 15, centerY: playPauseButton.centerY } ) );

    this.resetAllButton = new ResetAllButton( {
      listener: model.reset.bind( model ),
      scale: 0.85,
      centerX: this.controlPanel.centerX,

      // Align vertically with other controls, see #134
      centerY: (modelViewTransform.modelToViewY( 0 ) + this.layoutBounds.maxY) / 2 + 8,

      phetioID: options.tandem.createTandem( 'resetAllButton' )
    } );
    this.addChild( this.resetAllButton );

    // The button to return the skater
    this.returnSkaterButton = new RectangularPushButton( {
      content: new Text( controlsRestartSkaterString, {
        maxWidth: 100
      } ),
      listener: model.returnSkater.bind( model ),
      centerY: this.resetAllButton.centerY,
      // X updated in layoutBounds since the reset all button can move horizontally
      phetioID: options.tandem.createTandem( 'returnSkaterButton' )
    } );

    // Disable the return skater button when the skater is already at his initial coordinates
    model.skater.linkAttribute( 'moved', view.returnSkaterButton, 'enabled' );
    this.addChild( this.returnSkaterButton );

    this.addChild( new PlaybackSpeedControl( model.property( 'speed' ), {
      slowSpeedRadioButtonPhETIOID: options.tandem.createTandem( 'slowSpeedRadioButton' ),
      normalSpeedRadioButtonPhETIOID: options.tandem.createTandem( 'normalSpeedRadioButton' )
    } ).mutate( {
      left: stepButton.right + 20,
      centerY: playPauseButton.centerY
    } ) );

    var speedometerNode = new GaugeNode(
      // Hide the needle in for the background of the GaugeNode
      new Property( null ), propertiesSpeedString,
      {
        min: 0,
        max: 20
      },
      {
        // enable/disable updates based on whether the speedometer is visible
        updateEnabledProperty: model.property( 'speedometerVisible' ),
        pickable: false
      } );
    model.property( 'speedometerVisible' ).linkAttribute( speedometerNode, 'visible' );
    speedometerNode.centerX = this.layoutBounds.centerX;
    speedometerNode.top = this.layoutBounds.minY + 5;
    this.addChild( speedometerNode );

    // Layer which will contain all of the tracks
    var trackLayer = new Node();

    // Switch between selectable tracks
    if ( !model.draggableTracks ) {

      var trackNodes = model.tracks.map( function( track ) {
        return new TrackNode( model, track, modelViewTransform, view.availableModelBoundsProperty );
      } ).getArray();

      trackNodes.forEach( function( trackNode ) {
        trackLayer.addChild( trackNode );
      } );

      model.property( 'scene' ).link( function( scene ) {
        trackNodes[ 0 ].visible = (scene === 0);
        trackNodes[ 1 ].visible = (scene === 1);
        trackNodes[ 2 ].visible = (scene === 2);
      } );
    }
    else {

      var addTrackNode = function( track ) {

        var trackNode = new TrackNode( model, track, modelViewTransform, view.availableModelBoundsProperty );
        trackLayer.addChild( trackNode );

        // When track removed, remove its view
        var itemRemovedListener = function( removed ) {
          if ( removed === track ) {
            trackLayer.removeChild( trackNode );
            model.tracks.removeItemRemovedListener( itemRemovedListener );// Clean up memory leak
          }
        };
        model.tracks.addItemRemovedListener( itemRemovedListener );

        return trackNode;
      };

      // Create the tracks for the track toolbox
      var interactiveTrackNodes = model.tracks.map( addTrackNode ).getArray();

      // Add a panel behind the tracks
      var padding = 10;

      var trackCreationPanel = new Rectangle(
        (interactiveTrackNodes[ 0 ].left - padding / 2),
        (interactiveTrackNodes[ 0 ].top - padding / 2),
        (interactiveTrackNodes[ 0 ].width + padding),
        (interactiveTrackNodes[ 0 ].height + padding),
        6,
        6, {
          fill: 'white',
          stroke: 'black'
        } );
      this.addChild( trackCreationPanel );

      model.tracks.addItemAddedListener( addTrackNode );

      var xTip = 20;
      var yTip = 8;
      var xControl = 12;
      var yControl = -5;

      var createArrowhead = function( angle, tail ) {
        var headWidth = 10;
        var headHeight = 10;
        var directionUnitVector = Vector2.createPolar( 1, angle );
        var orthogonalUnitVector = directionUnitVector.perpendicular();
        var tip = directionUnitVector.times( headHeight ).plus( tail );
        return new Path( new Shape().moveToPoint( tail ).lineToPoint( tail.plus( orthogonalUnitVector.times( headWidth / 2 ) ) ).lineToPoint( tip ).lineToPoint( tail.plus( orthogonalUnitVector.times( -headWidth / 2 ) ) ).lineToPoint( tail ).close(),
          { fill: 'black' } );
      };

      var rightCurve = new Path( new Shape().moveTo( 0, 0 ).quadraticCurveTo( -xControl, yControl, -xTip, yTip ),
        { stroke: 'black', lineWidth: 3 } );
      var arrowHead = createArrowhead( Math.PI - Math.PI / 3, new Vector2( -xTip, yTip ) );

      var clearButtonEnabledProperty = model.property( 'clearButtonEnabled' );
      clearButtonEnabledProperty.link( function( clearButtonEnabled ) {
        rightCurve.stroke = clearButtonEnabled ? 'black' : 'gray';
        arrowHead.fill = clearButtonEnabled ? 'black' : 'gray';
      } );

      var clearButton = new EraserButton( {
        iconWidth: 30,
        baseColor: new Color( 221, 210, 32 ),
        phetioID: 'playgroundScreen.clearTracksButton'
      } );
      clearButtonEnabledProperty.linkAttribute( clearButton, 'enabled' );
      clearButton.addListener( function() {model.clearTracks();} );

      this.addChild( clearButton.mutate( { left: 5, centerY: trackCreationPanel.centerY } ) );
    }

    this.addChild( trackLayer );

    // Check to see if WebGL was prevented by a query parameter
    var allowWebGL = phet.chipper.getQueryParameter( 'webgl' ) !== 'false';

    // Use WebGL where available, but not on IE, due to https://github.com/phetsims/energy-skate-park-basics/issues/277
    // and https://github.com/phetsims/scenery/issues/285
    var webGLSupported = Util.isWebGLSupported && allowWebGL;
    var renderer = webGLSupported ? 'webgl' : null;

    var skaterNode = new SkaterNode(
      model.skater,
      this,
      modelViewTransform,
      model.getClosestTrackAndPositionAndParameter.bind( model ),
      model.getPhysicalTracks.bind( model ),
      renderer
    );

    var gaugeNeedleNode = new GaugeNeedleNode( model.skater.property( 'speed' ), {
      min: 0,
      max: 20
    }, {
      renderer: renderer
    } );
    model.property( 'speedometerVisible' ).linkAttribute( gaugeNeedleNode, 'visible' );
    gaugeNeedleNode.x = speedometerNode.x;
    gaugeNeedleNode.y = speedometerNode.y;
    this.addChild( gaugeNeedleNode );
    this.addChild( new BarGraphForeground( model.skater, barGraphBackground, model.property( 'barGraphVisible' ), renderer ) );
    this.addChild( skaterNode );

    var pieChartNode = renderer === 'webgl' ?
                       new PieChartWebGLNode( model.skater, model.property( 'pieChartVisible' ), modelViewTransform ) :
                       new PieChartNode( model.skater, model.property( 'pieChartVisible' ), modelViewTransform );
    this.addChild( pieChartNode );

    // Buttons to return the skater when she is offscreen, see #219
    var iconScale = 0.4;
    var returnSkaterToStartingPointButton = new RectangularPushButton( {
      content: new Image( skaterIconImage, { scale: iconScale } ),

      // green means "go" since the skater will likely start moving at this point
      baseColor: EnergySkateParkColorScheme.kineticEnergy,
      listener: model.returnSkater.bind( model ),
      phetioID: options.tandem.createTandem( 'returnSkaterToPreviousStartingPositionButton' )
    } );

    var returnSkaterToGroundButton = new RectangularPushButton( {
      content: new Image( skaterIconImage, { scale: iconScale } ),
      centerBottom: modelViewTransform.modelToViewPosition( model.skater.startingPosition ),
      baseColor: '#f4514e', // red for stop, since the skater will be stopped on the ground.
      listener: function() { model.skater.resetPosition(); },
      phetioID: options.tandem.createTandem( 'returnSkaterToGroundButton' )
    } );

    this.addChild( returnSkaterToStartingPointButton );
    this.addChild( returnSkaterToGroundButton );

    // When the skater goes off screen, make the "return skater" button big
    onscreenProperty.link( function( skaterOnscreen ) {
      var buttonsVisible = !skaterOnscreen;
      returnSkaterToGroundButton.visible = buttonsVisible;
      returnSkaterToStartingPointButton.visible = buttonsVisible;

      if ( buttonsVisible ) {

        // Put the button where the skater will appear.  Nudge it up a bit so the mouse can hit it from the drop site,
        // without being moved at all (to simplify repeat runs).
        var viewPosition = modelViewTransform.modelToViewPosition( model.skater.startingPosition ).plusXY( 0, 5 );
        returnSkaterToStartingPointButton.centerBottom = viewPosition;

        // If the return skater button went offscreen, move it back on the screen, see #222
        if ( returnSkaterToStartingPointButton.top < 5 ) {
          returnSkaterToStartingPointButton.top = 5;
        }
      }
    } );

    // For debugging the visible bounds
    if ( showAvailableBounds ) {
      this.viewBoundsPath = new Path( null, { pickable: false, stroke: 'red', lineWidth: 10 } );
      this.addChild( this.viewBoundsPath );
    }
  }
  function UnderPressureView( model ) {
    var self = this;
    ScreenView.call( this, { renderer: 'svg' } );

    var mvt = ModelViewTransform2.createSinglePointScaleMapping(
      Vector2.ZERO,
      Vector2.ZERO,
      70 ); //1m = 70px, (0,0) - top left corner

    //sky, earth and controls
    var backgroundNode = new BackgroundNode( model, mvt );
    this.addChild( backgroundNode );
    backgroundNode.moveToBack();

    var scenes = {};
    model.scenes.forEach( function( name ) {
      scenes[name] = new SceneView[name + 'PoolView']( model.sceneModels[name], mvt, self.layoutBounds );
      scenes[name].visible = false;
      self.addChild( scenes[name] );
    } );


    //control panel
    this.controlPanel = new ControlPanel( model, 625, 5 );
    this.addChild( this.controlPanel );

    //control sliders
    this.fluidDensitySlider = new ControlSlider( model, model.fluidDensityProperty, model.units.getFluidDensityString, model.fluidDensityRange, {
      x: 585,
      y: 250,
      title: fluidDensityString,
      ticks: [
        {
          title: WaterString,
          value: 1000
        },
        {
          title: GasolineString,
          value: model.fluidDensityRange.min
        },
        {
          title: HoneyString,
          value: model.fluidDensityRange.max
        }
      ]
    } );
    this.addChild( this.fluidDensitySlider );

    this.gravitySlider = new ControlSlider( model, model.gravityProperty, model.units.getGravityString, model.gravityRange, {
      x: 585,
      y: 360,
      title: gravityString,
      decimals: 1,
      ticks: [
        {
          title: EarthString,
          value: 9.8
        },
        {
          title: MarsString,
          value: model.gravityRange.min
        },
        {
          title: JupiterString,
          value: model.gravityRange.max
        }
      ]
    } );
    this.addChild( this.gravitySlider );

    model.mysteryChoiceProperty.link( function( choice, oldChoice ) {
      if ( model.currentScene === 'Mystery' ) {
        self[choice + 'Slider'].disable();
        if ( oldChoice ) {
          self[oldChoice + 'Slider'].enable();
        }
      }
    } );

    model.currentSceneProperty.link( function( currentScene ) {
      if ( currentScene === 'Mystery' ) {
        self[model.mysteryChoice + 'Slider'].disable();
      }
      else {
        self.gravitySlider.enable();
        self.fluidDensitySlider.enable();
      }
    } );

    // add reset button
    this.addChild( new ResetAllButton( {
      listener: function() { model.reset(); },
      scale: 0.66, x: 745, y: model.height - 25
    } ) );

    this.barometersContainer = new Rectangle( 0, 0, 100, 130, 10, 10, {stroke: 'gray', lineWidth: 1, fill: '#f2fa6a', x: 520, y: 5} );
    this.addChild( this.barometersContainer );

    this.addChild( new SceneChoiceNode( model, 10, 260 ) );

    //resize control panels
    var panels = [this.controlPanel, scenes.Mystery.mysteryPoolControls.choicePanel],
      maxWidth = 0;
    panels.forEach( function( panel ) {
      maxWidth = Math.max( maxWidth, panel.width / panel.transform.matrix.scaleVector.x );
    } );
    scenes.Mystery.mysteryPoolControls.choicePanel.resizeWidth( maxWidth );
    panels.forEach( function( panel ) {
      panel.centerX = self.gravitySlider.centerX;
    } );
    this.barometersContainer.x = this.controlPanel.x - 10 - this.barometersContainer.width;


    model.currentSceneProperty.link( function( value, oldValue ) {
      scenes[value].visible = true;
      if ( oldValue ) {
        scenes[oldValue].visible = false;
      }
    } );

    this.addChild( new UnderPressureRuler( model, mvt, self.layoutBounds ) );

    //barometers
    this.addChild( new BarometersContainer( model, mvt, this.barometersContainer.visibleBounds, self.layoutBounds ) );
  }
Example #24
0
  /**
   * @param {MicroModel} model
   * @param {ModelViewTransform2} modelViewTransform
   * @constructor
   */
  function MicroView( model, modelViewTransform ) {

    var thisView = this;
    ScreenView.call( thisView, PHScaleConstants.SCREEN_VIEW_OPTIONS );

    // view-specific properties
    var viewProperties = new PropertySet( {
      ratioVisible: false,
      moleculeCountVisible: false,
      pHMeterExpanded: true,
      graphExpanded: true
    } );

    // beaker
    var beakerNode = new BeakerNode( model.beaker, modelViewTransform );
    var solutionNode = new SolutionNode( model.solution, model.beaker, modelViewTransform );
    var volumeIndicatorNode = new VolumeIndicatorNode( model.solution.volumeProperty, model.beaker, modelViewTransform );

    // dropper
    var DROPPER_SCALE = 0.85;
    var dropperNode = new DropperNode( model.dropper, modelViewTransform );
    dropperNode.setScaleMagnitude( DROPPER_SCALE );
    var dropperFluidNode = new DropperFluidNode( model.dropper, model.beaker, DROPPER_SCALE * dropperNode.getTipWidth(), modelViewTransform );

    // faucets
    var waterFaucetNode = new WaterFaucetNode( model.waterFaucet, modelViewTransform );
    var drainFaucetNode = new DrainFaucetNode( model.drainFaucet, modelViewTransform );
    var SOLVENT_FLUID_HEIGHT = model.beaker.location.y - model.waterFaucet.location.y;
    var DRAIN_FLUID_HEIGHT = 1000; // tall enough that resizing the play area is unlikely to show bottom of fluid
    var waterFluidNode = new FaucetFluidNode( model.waterFaucet, new Property( Water.color ), SOLVENT_FLUID_HEIGHT, modelViewTransform );
    var drainFluidNode = new FaucetFluidNode( model.drainFaucet, model.solution.colorProperty, DRAIN_FLUID_HEIGHT, modelViewTransform );

    // 'H3O+/OH- ratio' representation
    var ratioNode = new RatioNode( model.beaker, model.solution, modelViewTransform, { visible: viewProperties.ratioVisibleProperty.get() } );
    viewProperties.ratioVisibleProperty.linkAttribute( ratioNode, 'visible' );

    // 'molecule count' representation
    var moleculeCountNode = new MoleculeCountNode( model.solution );
    viewProperties.moleculeCountVisibleProperty.linkAttribute( moleculeCountNode, 'visible' );

    // beaker controls
    var beakerControls = new BeakerControls( viewProperties.ratioVisibleProperty, viewProperties.moleculeCountVisibleProperty );

    // graph
    var graphNode = new GraphNode( model.solution, viewProperties.graphExpandedProperty, {
      hasLinearFeature: true,
      logScaleHeight: 485,
      linearScaleHeight: 440
    } );

    // pH meter
    var pHMeterTop = 15;
    var pHMeterNode = new PHMeterNode( model.solution, modelViewTransform.modelToViewY( model.beaker.location.y ) - pHMeterTop, viewProperties.pHMeterExpandedProperty,
      { attachProbe: 'right' } );

    // solutes combo box
    var soluteListParent = new Node();
    var soluteComboBox = new SoluteComboBox( model.solutes, model.dropper.soluteProperty, soluteListParent );

    var resetAllButton = new ResetAllButton( {
      scale: 1.32,
      listener: function() {
        model.reset();
        viewProperties.reset();
        graphNode.reset();
      }
    } );

    // Parent for all nodes added to this screen
    var rootNode = new Node( { children: [
      // nodes are rendered in this order
      waterFluidNode,
      waterFaucetNode,
      drainFluidNode,
      drainFaucetNode,
      dropperFluidNode,
      dropperNode,
      solutionNode,
      pHMeterNode,
      ratioNode,
      beakerNode,
      moleculeCountNode,
      volumeIndicatorNode,
      beakerControls,
      graphNode,
      resetAllButton,
      soluteComboBox,
      soluteListParent // last, so that combo box list is on top
    ] } );
    thisView.addChild( rootNode );

    // Layout of nodes that don't have a location specified in the model
    moleculeCountNode.centerX = beakerNode.centerX;
    moleculeCountNode.bottom = beakerNode.bottom - 25;
    beakerControls.centerX = beakerNode.centerX;
    beakerControls.top = beakerNode.bottom + 10;
    pHMeterNode.left = modelViewTransform.modelToViewX( model.beaker.left ) - ( 0.4 * pHMeterNode.width );
    pHMeterNode.top = pHMeterTop;
    graphNode.right = drainFaucetNode.left - 40;
    graphNode.top = pHMeterNode.top;
    soluteComboBox.left = pHMeterNode.right + 35;
    soluteComboBox.top = this.layoutBounds.top + pHMeterTop;
    resetAllButton.right = this.layoutBounds.right - 40;
    resetAllButton.bottom = this.layoutBounds.bottom - 20;
  }
Example #25
0
  /**
   * @param {Object[]} demos - each demo has these properties:
   *   {string} label - label in the combo box
   *   {function(layoutBounds:Bounds2): Node} createNode - creates the Node for the demo
   *   {Node|null} node - the Node for the demo, created by DemosScreenView
   * @param {Object} [options]
   * @constructor
   */
  function DemosScreenView( demos, options ) {

    options = _.extend( {

      selectedDemoLabel: null, // {string|null} label field of the demo to be selected initially

      // options related to the ComboBox that selects the demo
      comboBoxCornerRadius: 4,
      comboBoxLocation: new Vector2( 20, 20 ), // {Vector2} location of ComboBox used to select a demo
      comboBoxItemFont: new PhetFont( 20 ), // {Font} font used for ComboBox items
      comboBoxItemXMargin: 12, // {number} x margin around ComboBox items
      comboBoxItemYMargin: 8, // {number} y margin around ComboBox items

      // {boolean} see https://github.com/phetsims/sun/issues/386
      // true = caches Nodes for all demos that have been selected
      // false = keeps only the Node for the selected demo in memory
      cacheDemos: false,

      tandem: Tandem.required
    }, options );

    ScreenView.call( this, options );

    var layoutBounds = this.layoutBounds;

    // Sort the demos by label, so that they appear in the combo box in alphabetical order
    demos = _.sortBy( demos, function( demo ) {
      return demo.label;
    } );

    // All demos will be children of this node, to maintain rendering order with combo box list
    var demosParent = new Node();
    this.addChild( demosParent );

    // add each demo to the combo box
    var comboBoxItems = [];
    demos.forEach( function( demo ) {
      comboBoxItems.push( new ComboBoxItem( new Text( demo.label, { font: options.comboBoxItemFont } ), demo, {

        // demo.label is like ArrowNode or TimerNode, decorate it for use as a tandem.
        tandemName: 'demo' + demo.label + 'ComboBoxItem'
      } ) );
    } );

    // Parent for the combo box popup list
    var listParent = new Node();
    this.addChild( listParent );

    // Set the initial demo
    var selectedDemo = demos[ 0 ];
    if ( options.selectedDemoLabel ) {
      selectedDemo = _.find( demos, function( demo ) {
        return ( demo.label === options.selectedDemoLabel );
      } );
      if ( !selectedDemo ) {
        throw new Error( 'demo not found: ' + options.selectedDemoLabel );
      }
    }

    // Combo box for selecting which component to view
    var selectedDemoProperty = new Property( selectedDemo );
    var comboBox = new ComboBox( comboBoxItems, selectedDemoProperty, listParent, {
      buttonFill: 'rgb( 218, 236, 255 )',
      cornerRadius: options.comboBoxCornerRadius,
      xMargin: options.comboBoxItemXMargin,
      yMargin: options.comboBoxItemYMargin,
      top: options.comboBoxLocation.x,
      left: options.comboBoxLocation.y,
      tandem: options.tandem.createTandem( 'comboBox' )
    } );
    this.addChild( comboBox );

    // Make the selected demo visible
    selectedDemoProperty.link( function( demo, oldDemo ) {

      // clean up the previously selected demo
      if ( oldDemo ) {
        if ( options.cacheDemos ) {

          // make the old demo invisible
          oldDemo.node.visible = false;
        }
        else {

          // delete the old demo
          demosParent.removeChild( oldDemo.node );
          oldDemo.node.dispose();
          oldDemo.node = null;
        }
      }

      if ( demo.node ) {

        // If the selected demo has an associated node, make it visible.
        demo.node.visible = true;
      }
      else {

        // If the selected demo doesn't doesn't have an associated node, create it.
        demo.node = demo.createNode( layoutBounds, {
          tandem: options.tandem.createTandem( 'demo' + demo.label )
        } );
        demosParent.addChild( demo.node );
      }
    } );
  }
  /**
   * @param {MakeIsotopesModel} makeIsotopesModel
   * @param {Tandem} tandem
   * @constructor
   */
  function MakeIsotopesScreenView( makeIsotopesModel, tandem ) {
    // super type constructor
    ScreenView.call( this, { layoutBounds: ShredConstants.LAYOUT_BOUNDS } );

    // Set up the model-canvas transform.  IMPORTANT NOTES: The multiplier factors for the point in the view can be
    // adjusted to shift the center right or left, and the scale factor can be adjusted to zoom in or out (smaller
    // numbers zoom out, larger ones zoom in).
    this.modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( Vector2.ZERO,
      new Vector2( Util.roundSymmetric( this.layoutBounds.width * 0.4 ),
        Util.roundSymmetric( this.layoutBounds.height * 0.49 ) ),
      1.0
    );

    // Layers upon which the various display elements are placed. This allows us to create the desired layering effects.
    var indicatorLayer = new Node();
    this.addChild( indicatorLayer );
    //adding this layer later so that its on the top
    var atomLayer = new Node();

    // Create and add the Reset All Button in the bottom right, which resets the model
    var resetAllButton = new ResetAllButton( {
      listener: function() {
        makeIsotopesModel.reset();
        scaleNode.reset();
        symbolBox.expandedProperty.reset();
        abundanceBox.expandedProperty.reset();
      },
      right: this.layoutBounds.maxX - 10,
      bottom: this.layoutBounds.maxY - 10
    } );
    resetAllButton.scale( 0.85 );
    this.addChild( resetAllButton );


    // Create the node that represents the scale upon which the atom sits.
    var scaleNode = new AtomScaleNode( makeIsotopesModel.particleAtom );

    // The scale needs to sit just below the atom, and there are some "tweak factors" needed to get it looking right.
    scaleNode.setCenterBottom( new Vector2( this.modelViewTransform.modelToViewX( 0 ), this.bottom ) );
    this.addChild( scaleNode );

    // Create the node that contains both the atom and the neutron bucket.
    var bottomOfAtomPosition = new Vector2( scaleNode.centerX, scaleNode.top + 15 ); //empirically determined

    var atomAndBucketNode = new InteractiveIsotopeNode( makeIsotopesModel, this.modelViewTransform, bottomOfAtomPosition );
    atomLayer.addChild( atomAndBucketNode );

    // Add the interactive periodic table that allows the user to select the current element.  Heaviest interactive
    // element is Neon for this sim.
    var periodicTableNode = new ExpandedPeriodicTableNode( makeIsotopesModel.numberAtom, 10, {
      tandem: tandem
    } );
    periodicTableNode.scale( 0.65 );
    periodicTableNode.top = 10;
    periodicTableNode.right = this.layoutBounds.width - 10;
    this.addChild( periodicTableNode );

    // Add the legend/particle count indicator.
    var particleCountLegend = new ParticleCountDisplay( makeIsotopesModel.particleAtom, 13, 250 );
    particleCountLegend.scale( 1.1 );
    particleCountLegend.left = 20;
    particleCountLegend.top = periodicTableNode.visibleBounds.minY;
    indicatorLayer.addChild( particleCountLegend );

    var symbolRectangle = new Rectangle( 0, 0, SYMBOL_BOX_WIDTH, SYMBOL_BOX_HEIGHT, 0, 0, {
      fill: 'white',
      stroke: 'black',
      lineWidth: 2
    } );

    // Add the symbol text.
    var symbolText = new Text( '', {
      font: new PhetFont( 150 ),
      fill: 'black',
      center: new Vector2( symbolRectangle.width / 2, symbolRectangle.height / 2 )
    } );

    // Add the listener to update the symbol text.
    var textCenter = new Vector2( symbolRectangle.width / 2, symbolRectangle.height / 2 );
    // Doesn't need unlink as it stays through out the sim life
    makeIsotopesModel.particleAtom.protonCountProperty.link( function( protonCount ) {
      var symbol = AtomIdentifier.getSymbol( protonCount );
      symbolText.text = protonCount > 0 ? symbol : '';
      symbolText.center = textCenter;
    } );
    symbolRectangle.addChild( symbolText );

    // Add the proton count display.
    var protonCountDisplay = new Text( '0', {
      font: NUMBER_FONT,
      fill: 'red'
    } );
    symbolRectangle.addChild( protonCountDisplay );

    // Add the listener to update the proton count.
    // Doesn't need unlink as it stays through out the sim life
    makeIsotopesModel.particleAtom.protonCountProperty.link( function( protonCount ) {
      protonCountDisplay.text = protonCount;
      protonCountDisplay.left = NUMBER_INSET;
      protonCountDisplay.bottom = SYMBOL_BOX_HEIGHT - NUMBER_INSET;
    } );

    // Add the mass number display.
    var massNumberDisplay = new Text( '0', {
      font: NUMBER_FONT,
      fill: 'black'
    } );
    symbolRectangle.addChild( massNumberDisplay );

    // Add the listener to update the mass number.
    // Doesn't need unlink as it stays through out the sim life
    makeIsotopesModel.particleAtom.massNumberProperty.link( function( massNumber ) {
      massNumberDisplay.text = massNumber;
      massNumberDisplay.left = NUMBER_INSET;
      massNumberDisplay.top = NUMBER_INSET;
    } );

    symbolRectangle.scale( 0.20 );
    var symbolBox = new AccordionBox( symbolRectangle, {
      cornerRadius: 3,
      titleNode: new Text( symbolString, {
        font: ShredConstants.ACCORDION_BOX_TITLE_FONT,
        maxWidth: ShredConstants.ACCORDION_BOX_TITLE_MAX_WIDTH
      } ),
      fill: ShredConstants.DISPLAY_PANEL_BACKGROUND_COLOR,
      expandedProperty: new Property( false ),
      minWidth: periodicTableNode.visibleBounds.width,
      contentAlign: 'center',
      titleAlignX: 'left',
      buttonAlign: 'right',
      expandCollapseButtonOptions: {
        touchAreaXDilation: OPEN_CLOSE_BUTTON_TOUCH_AREA_DILATION,
        touchAreaYDilation: OPEN_CLOSE_BUTTON_TOUCH_AREA_DILATION
      }
    } );
    symbolBox.left = periodicTableNode.visibleBounds.minX;
    symbolBox.top = periodicTableNode.bottom + 10;
    this.addChild( symbolBox );

    var abundanceBox = new AccordionBox( new TwoItemPieChartNode( makeIsotopesModel ), {
      cornerRadius: 3,
      titleNode: new Text( abundanceInNatureString, {
        font: ShredConstants.ACCORDION_BOX_TITLE_FONT,
        maxWidth: ShredConstants.ACCORDION_BOX_TITLE_MAX_WIDTH
      } ),
      fill: ShredConstants.DISPLAY_PANEL_BACKGROUND_COLOR,
      expandedProperty: new Property( false ),
      minWidth: periodicTableNode.visibleBounds.width,
      contentAlign: 'center',
      contentXMargin: 0,
      titleAlignX: 'left',
      buttonAlign: 'right',
      expandCollapseButtonOptions: {
        touchAreaXDilation: OPEN_CLOSE_BUTTON_TOUCH_AREA_DILATION,
        touchAreaYDilation: OPEN_CLOSE_BUTTON_TOUCH_AREA_DILATION
      }
    } );
    abundanceBox.left = symbolBox.left;
    abundanceBox.top = symbolBox.bottom + 10;
    this.addChild( abundanceBox );
    this.addChild( atomLayer );
  }
  /**
   * @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
    ];
  }
  /**
   * @param {CircuitConstructionKitModel} introScreenModel
   * @constructor
   */
  function IntroScreenView( introScreenModel, tandem ) {
    var self = this;
    this.introScreenModel = introScreenModel;
    ScreenView.call( this );

    this.sceneNodes = {};

    var sceneSelectionRadioButtonGroup = new SceneSelectionRadioButtonGroup(
      introScreenModel.selectedSceneProperty
    );

    // Reset All button
    var resetAllButton = new ResetAllButton( {
      listener: function() {
        introScreenModel.reset();

        _.values( self.sceneNodes ).forEach( function( sceneNode ) {
          sceneNode.reset();
          sceneNode.model.reset();
        } );
      }
    } );
    this.addChild( resetAllButton );

    introScreenModel.selectedSceneProperty.link( function( selectedScene ) {
      if ( !self.sceneNodes[ selectedScene ] ) {
        var options = {
          0: {
            numberOfRightBatteriesInToolbox: 1,
            numberOfWiresInToolbox: 4,
            numberOfLightBulbsInToolbox: 0,
            numberOfResistorsInToolbox: 0,
            numberOfSwitchesInToolbox: 0
          },
          1: {
            numberOfRightBatteriesInToolbox: 1,
            numberOfWiresInToolbox: 4,
            numberOfLightBulbsInToolbox: 0,
            numberOfResistorsInToolbox: 0,
            numberOfSwitchesInToolbox: 0
          },
          2: {
            numberOfRightBatteriesInToolbox: 1,
            numberOfWiresInToolbox: 4,
            numberOfLightBulbsInToolbox: 0,
            numberOfResistorsInToolbox: 0,
            numberOfSwitchesInToolbox: 1
          }
        }[ selectedScene ];
        var sceneNode = new IntroSceneNode(
          new IntroSceneModel( self.layoutBounds, introScreenModel.selectedSceneProperty ),
          tandem.createTandem( selectedScene ),
          options );
        sceneNode.visibleBoundsProperty.set( self.visibleBoundsProperty.value );
        self.sceneNodes[ selectedScene ] = sceneNode;
      }

      // scene selection buttons go in back so that circuit elements may go in front
      self.children = [
        resetAllButton,
        sceneSelectionRadioButtonGroup,
        self.sceneNodes[ selectedScene ]
      ];
    } );

    this.visibleBoundsProperty.link( function( visibleBounds ) {
      _.values( self.sceneNodes ).forEach( function( sceneNode ) {
        sceneNode.visibleBoundsProperty.set( visibleBounds );
      } );

      sceneSelectionRadioButtonGroup.mutate( {
        left: visibleBounds.left + LAYOUT_INSET,
        top: visibleBounds.top + LAYOUT_INSET
      } );

      // Float the resetAllButton to the bottom right
      resetAllButton.mutate( {
        right: visibleBounds.right - LAYOUT_INSET,
        bottom: visibleBounds.bottom - LAYOUT_INSET
      } );
    } );
  }
  /**
   * @param {FaradaysLawModel} model - Faraday's Law simulation model object
   * @param {Tandem} tandem
   * @constructor
   */
  function FaradaysLawScreenView( model, tandem ) {
    ScreenView.call( this, {
      layoutBounds: FaradaysLawConstants.LAYOUT_BOUNDS,

      // a11y - TODO: Remove once https://github.com/phetsims/scenery-phet/issues/393 is complete
      addScreenSummaryNode: true
    } );

    // screen Summary
    var summaryNode = new Node();
    summaryNode.addChild( new Node( { tagName: 'p', innerContent: summaryDescriptionString } ) );
    summaryNode.addChild( new Node( { tagName: 'p', innerContent: moveMagnetToPlayString } ) );

    var playArea = new PlayAreaNode( { containerTagName: null } );

    var circuitDescriptionNode = new CircuitDescriptionNode( model );

    playArea.addChild( circuitDescriptionNode );

    this.screenSummaryNode.addChild( summaryNode );
    this.addChild( playArea );

    // coils
    var bottomCoilNode = new CoilNode( CoilTypeEnum.FOUR_COIL, {
      x: model.bottomCoil.position.x,
      y: model.bottomCoil.position.y
    } );

    var topCoilNode = new CoilNode( CoilTypeEnum.TWO_COIL, {
      x: model.topCoil.position.x,
      y: model.topCoil.position.y
    } );

    // @public {Vector2[]}
    this.bottomCoilEndPositions = {
      topEnd: bottomCoilNode.endRelativePositions.topEnd.plus( model.bottomCoil.position ),
      bottomEnd: bottomCoilNode.endRelativePositions.bottomEnd.plus( model.bottomCoil.position )
    };

    // @public {Vector2[]}
    this.topCoilEndPositions = {
      topEnd: topCoilNode.endRelativePositions.topEnd.plus( model.topCoil.position ),
      bottomEnd: topCoilNode.endRelativePositions.bottomEnd.plus( model.topCoil.position )
    };

    // voltmeter and bulb created
    var voltmeterAndWiresNode = new VoltmeterAndWiresNode( model.voltmeter.needleAngleProperty, tandem.createTandem( 'voltmeterNode' ) );
    var bulbNode = new BulbNode( model.voltageProperty, {
      center: FaradaysLawConstants.BULB_POSITION
    } );

    // wires
    this.addChild( new CoilsWiresNode( this, model.topCoilVisibleProperty ) );

    // exists for the lifetime of the sim, no need to dispose
    model.voltmeterVisibleProperty.link( function( showVoltmeter ) {
      voltmeterAndWiresNode.visible = showVoltmeter;
    } );

    // When PhET-iO Studio makes the voltmeter invisible, we should also uncheck the checkbox.
    voltmeterAndWiresNode.on( 'visibility', function() {
      model.voltmeterVisibleProperty.value = voltmeterAndWiresNode.visible;
    } );

    // bulb added
    this.addChild( bulbNode );

    // coils added
    this.addChild( bottomCoilNode );
    this.addChild( topCoilNode );
    model.topCoilVisibleProperty.linkAttribute( topCoilNode, 'visible' );

    // control panel
    var controlPanel = new ControlPanelNode( model, tandem );
    this.addChild( controlPanel );

    // voltmeter added
    this.addChild( voltmeterAndWiresNode );

    // @private
    this.magnetNodeWithField = new MagnetNodeWithField( model, tandem.createTandem( 'magnetNode' ) );
    this.addChild( this.magnetNodeWithField );

    // a11y keyboard nav order
    this.accessibleOrder = [
      this.screenSummaryNode,
      playArea,
      this.magnetNodeWithField,
      controlPanel
    ];

    // move coils to front
    bottomCoilNode.frontImage.detach();
    this.addChild( bottomCoilNode.frontImage );
    bottomCoilNode.frontImage.center = model.bottomCoil.position.plus( new Vector2( CoilNode.xOffset, 0 ) );

    topCoilNode.frontImage.detach();
    this.addChild( topCoilNode.frontImage );
    topCoilNode.frontImage.center = model.topCoil.position.plus( new Vector2( CoilNode.xOffset + CoilNode.twoOffset, 0 ) );
    model.topCoilVisibleProperty.linkAttribute( topCoilNode.frontImage, 'visible' );

    // const tcInnerBounds = Shape.bounds( this.magnetNodeWithField.regionManager._bottomCoilInnerBounds ).getStrokedShape();

    // this.addChild( Rectangle.bounds( this.magnetNodeWithField.regionManager._topCoilInnerBounds, { stroke: 'red' } ) )
    // this.addChild( Rectangle.bounds( this.magnetNodeWithField.regionManager._bottomCoilInnerBounds, { stroke: 'red' } ) )

  }
  /**
   * @param {MultipleCellsModel} model
   * @constructor
   */
  function MultipleCellsScreenView( model ) {
    ScreenView.call( this );
    var self = this;
    this.model = model;

    // Set up the model-canvas transform. The multiplier factors for the 2nd point can be adjusted to shift the center
    // right or left, and the scale factor can be adjusted to zoom in or out (smaller numbers zoom out, larger ones zoom
    // in).
    this.modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping(
      Vector2.ZERO,
      new Vector2( this.layoutBounds.width * 0.455, this.layoutBounds.height * 0.56 ),
      1E8 // "zoom factor" - smaller zooms out, larger zooms in
    );

    // dialog constructed lazily because Dialog requires Sim bounds during construction
    var dialog = null;

    var buttonContent = new Text( showRealCellsString, {
      font: new PhetFont( 18 ),
      maxWidth: 140
    } );
    var showRealCellsButton = new RectangularPushButton( {
      content: buttonContent,
      touchAreaXDilation: 7,
      touchAreaYDilation: 7,
      baseColor: 'yellow',
      cornerRadius: GEEConstants.CORNER_RADIUS,
      listener: function() {
        if ( !dialog ) {
          dialog = new FluorescentCellsPictureDialog();
        }
        dialog.show();
      }
    } );

    showRealCellsButton.left = this.layoutBounds.minX + 10;
    showRealCellsButton.top = this.layoutBounds.minY + 10;
    this.addChild( showRealCellsButton );

    this.proteinLevelChartNode = new ProteinLevelChartNode( model.averageProteinLevelProperty );
    this.addChild( this.proteinLevelChartNode );
    this.proteinLevelChartNode.top = showRealCellsButton.top;
    this.proteinLevelChartNode.left = showRealCellsButton.right + 10;

    // Add the Reset All button.
    var resetAllButton = new ResetAllButton( {
      listener: function() {
        model.reset();
        concentrationControlPanel.expandedProperty.reset();
        affinityControlPanel.expandedProperty.reset();
        degradationControlPanel.expandedProperty.reset();
        self.proteinLevelChartNode.reset();
      },
      right: this.layoutBounds.maxX - 10,
      bottom: this.layoutBounds.maxY - 10
    } );
    this.addChild( resetAllButton );

    // Add play/pause button.
    var playPauseButton = new PlayPauseButton( model.clockRunningProperty, {
      radius: 23,
      touchAreaDilation: 5
    } );
    this.addChild( playPauseButton );

    var stepButton = new StepForwardButton( {
      isPlayingProperty: model.clockRunningProperty,
      listener: function() {
        model.stepInTime( 0.016 );
        self.proteinLevelChartNode.addDataPoint( 0.016 );
      },
      radius: 15,
      touchAreaDilation: 5
    } );
    this.addChild( stepButton );

    playPauseButton.bottom = resetAllButton.bottom;
    stepButton.centerY = playPauseButton.centerY;

    var cellLayer = new Node();
    var invisibleCellLayer = new Node(); // for performance improvement load all cells at start of the sim
    this.addChild( cellLayer );

    var cellNumberController = new ControllerNode(
      model.numberOfVisibleCellsProperty,
      1,
      MultipleCellsModel.MaxCells,
      oneString,
      manyString
    );

    var cellNumberControllerNode = new Node();
    cellNumberControllerNode.addChild( cellNumberController );

    var cellNumberLabel = new Text( cellsString, {
      font: new PhetFont( { size: 15, weight: 'bold' } ),
      maxWidth: 100
    } );

    cellNumberControllerNode.addChild( cellNumberLabel );
    cellNumberLabel.centerX = cellNumberController.centerX;
    cellNumberLabel.bottom = cellNumberController.top - 5;

    var cellNumberControllerPanel = new Panel( cellNumberControllerNode, {
      cornerRadius: GEEConstants.CORNER_RADIUS,
      xMargin: 10,
      yMargin: 10,
      fill: new Color( 220, 236, 255 )
    } );

    this.addChild( cellNumberControllerPanel );
    cellNumberControllerPanel.bottom = resetAllButton.bottom;
    cellNumberControllerPanel.centerX = this.proteinLevelChartNode.centerX;

    var cellNodes = [];

    for ( var i = 0; i < model.cellList.length; i++ ) {
      var cellNode = new ColorChangingCellNode( model.cellList[ i ], this.modelViewTransform );
      cellNodes.push( cellNode );
      invisibleCellLayer.addChild( cellNode );
    }

    function addCellView( addedCellIndex ) {
      cellLayer.addChild( cellNodes[ addedCellIndex ] );

      model.visibleCellList.addItemRemovedListener( function removalListener( removedCell ) {
        var removedCellIndex = model.cellList.indexOf( removedCell );
        if ( removedCellIndex === addedCellIndex ) {
          cellLayer.removeChild( cellNodes[ addedCellIndex ] );
          model.visibleCellList.removeItemRemovedListener( removalListener );
          cellLayer.setScaleMagnitude( 1 );
          var scaleFactor = Math.min( ( cellNumberControllerPanel.top - self.proteinLevelChartNode.bottom ) / cellLayer.height, 1 );
          cellLayer.setScaleMagnitude( scaleFactor * 0.9 );
          cellLayer.centerX = self.proteinLevelChartNode.centerX;
          cellLayer.centerY = self.proteinLevelChartNode.bottom +
                              ( cellNumberControllerPanel.top - self.proteinLevelChartNode.bottom ) / 2;
        }
      } );
      cellLayer.setScaleMagnitude( 1 );
      var scaleFactor = Math.min( ( cellNumberControllerPanel.top - self.proteinLevelChartNode.bottom ) / cellLayer.height, 1 );
      cellLayer.setScaleMagnitude( scaleFactor * 0.9 );
      cellLayer.centerX = self.proteinLevelChartNode.centerX;
      cellLayer.centerY = self.proteinLevelChartNode.bottom +
                          ( cellNumberControllerPanel.top - self.proteinLevelChartNode.bottom ) / 2;

    }

    // Set up an observer of the list of cells in the model so that the view representations can come and go as needed.
    model.visibleCellList.addItemAddedListener( function( addedCell ) {
      addCellView( model.cellList.indexOf( addedCell ) );
    } );

    model.visibleCellList.forEach( function( cell ) {
      addCellView( model.cellList.indexOf( cell ) );
    } );

    var concentrationControllers = [
      {
        label: positiveTranscriptionFactorString,
        controlProperty: model.transcriptionFactorLevelProperty,
        minValue: CellProteinSynthesisSimulator.TranscriptionFactorCountRange.min,
        maxValue: CellProteinSynthesisSimulator.TranscriptionFactorCountRange.max,
        minLabel: lowString,
        maxLabel: highString,
        logScale: true
      },
      {
        label: mRnaDestroyerString,
        controlProperty: model.mRnaDegradationRateProperty,
        minValue: CellProteinSynthesisSimulator.MRNADegradationRateRange.min,
        maxValue: CellProteinSynthesisSimulator.MRNADegradationRateRange.max,
        minLabel: lowString,
        maxLabel: highString,
        logScale: true
      }
    ];

    var concentrationControlPanel = new ControlPanelNode(
      concentrationString,
      concentrationControllers
    );

    var affinityControllers = [
      {
        label: positiveTranscriptionFactorString,
        controlProperty: model.transcriptionFactorAssociationProbabilityProperty,
        minValue: CellProteinSynthesisSimulator.TFAssociationProbabilityRange.min,
        maxValue: CellProteinSynthesisSimulator.TFAssociationProbabilityRange.max,
        minLabel: lowString,
        maxLabel: highString,
        logScale: true
      },
      {
        label: polymeraseString,
        controlProperty: model.polymeraseAssociationProbabilityProperty,
        minValue: CellProteinSynthesisSimulator.PolymeraseAssociationProbabilityRange.min,
        maxValue: CellProteinSynthesisSimulator.PolymeraseAssociationProbabilityRange.max,
        minLabel: lowString,
        maxLabel: highString,
        logScale: false
      }
    ];

    var affinityControlPanel = new ControlPanelNode(
      affinitiesString,
      affinityControllers
    );

    var degradationControllers = [
      {
        label: proteinString,
        controlProperty: model.proteinDegradationRateProperty,
        minValue: CellProteinSynthesisSimulator.ProteinDegradationRange.min,
        maxValue: CellProteinSynthesisSimulator.ProteinDegradationRange.max,
        minLabel: slowString,
        maxLabel: fastString,
        logScale: false
      }
    ];

    var degradationControlPanel = new ControlPanelNode(
      degradationString,
      degradationControllers
    );

    this.addChild( concentrationControlPanel );
    this.addChild( affinityControlPanel );
    this.addChild( degradationControlPanel );

    concentrationControlPanel.right = this.layoutBounds.maxX - 10;
    concentrationControlPanel.top = this.layoutBounds.minY + 10;

    affinityControlPanel.right = concentrationControlPanel.right;
    affinityControlPanel.top = concentrationControlPanel.bottom + 10;

    degradationControlPanel.right = affinityControlPanel.right;
    degradationControlPanel.top = affinityControlPanel.bottom + 10;

    playPauseButton.bottom = resetAllButton.bottom;
    stepButton.centerY = playPauseButton.centerY;
    stepButton.right = degradationControlPanel.left - 20;
    playPauseButton.right = stepButton.left - 10;
  }