function MassReadoutNode( bodyNode, visibleProperty ) {
    Node.call( this );
    var self = this;
    this.bodyNode = bodyNode; // @protected

    var readoutText = new Text( this.createText(), {
      pickable: false,
      font: new PhetFont( 18 ),
      fill: GravityAndOrbitsColorProfile.bodyNodeTextProperty
    } );
    this.addChild( readoutText );

    var updateLocation = function() {
      var bounds = bodyNode.bodyRenderer.getBounds();

      self.x = bounds.centerX - self.width / 2;
      if ( bodyNode.body.massReadoutBelow ) {
        self.y = bounds.maxX + self.height;
      }
      else {
        self.y = bounds.minY - self.height;
      }
    };

    bodyNode.body.massProperty.link( function() {
      readoutText.setText( self.createText() );
      updateLocation();
    } );

    visibleProperty.link( function( visible ) {
      // set visible and update location
      self.visible = visible;
      updateLocation();
    } );
  }
  /*
   * Constructor for TrackNode
   * @param {EnergySkateParkBasicsModel} model the entire model.  Not absolutely necessary, but so many methods are called on it for joining and
   * splitting tracks that we pass the entire model anyways.
   * @param {Track} track the track for this track node
   * @param {ModelViewTransform} modelViewTransform the model view transform for the view
   * @constructor
   */
  function TrackNode( model, track, modelViewTransform, availableBoundsProperty ) {
    var trackNode = this;
    this.track = track;
    this.model = model;
    this.modelViewTransform = modelViewTransform;
    this.availableBoundsProperty = availableBoundsProperty;

    this.road = new Path( null, { fill: 'gray', cursor: track.interactive ? 'pointer' : 'default' } );
    this.centerLine = new Path( null, { stroke: 'black', lineWidth: 1.2, lineDash: [ 11, 8 ] } );
    model.property( 'detachable' ).link( function( detachable ) {
      trackNode.centerLine.lineDash = detachable ? null : [ 11, 8 ];
    } );

    Node.call( this, { children: [ this.road, this.centerLine ] } );

    // Reuse arrays to save allocations and prevent garbage collections, see #38
    this.xArray = new FastArray( track.controlPoints.length );
    this.yArray = new FastArray( track.controlPoints.length );

    // Store for performance
    this.lastPoint = (track.controlPoints.length - 1) / track.controlPoints.length;

    // Sample space, which is recomputed if the track gets longer, to keep it looking smooth no matter how many control points
    this.linSpace = numeric.linspace( 0, this.lastPoint, 20 * (track.controlPoints.length - 1) );
    this.lengthForLinSpace = track.controlPoints.length;

    //If the track is interactive, make it draggable and make the control points visible and draggable
    if ( track.interactive ) {

      var trackDragHandler = new TrackDragHandler( this );
      this.road.addInputListener( trackDragHandler );

      for ( var i = 0; i < track.controlPoints.length; i++ ) {
        var isEndPoint = i === 0 || i === track.controlPoints.length - 1;
        trackNode.addChild( new ControlPointNode( trackNode, trackDragHandler, i, isEndPoint ) );
      }
    }

    // Init the track shape
    this.updateTrackShape();

    // Update the track shape when the whole track is translated
    // Just observing the control points individually would lead to N expensive callbacks (instead of 1) for each of the N points
    // So we use this broadcast mechanism instead
    track.on( 'translated', this.updateTrackShape.bind( this ) );

    track.draggingProperty.link( function( dragging ) {
      if ( !dragging ) {
        trackNode.updateTrackShape();
      }
    } );

    track.on( 'reset', this.updateTrackShape.bind( this ) );
    track.on( 'smoothed', this.updateTrackShape.bind( this ) );
    track.on( 'update', this.updateTrackShape.bind( this ) );
  }
Example #3
0
  /**
   * @param {Bounds2} layoutBounds
   * @param {Property.<Bounds2>} visibleBoundsProperty - visible bounds of the parent ScreenView
   * @param {Object} [options]
   * @constructor
   */
  function StatusBar( layoutBounds, visibleBoundsProperty, options ) {

    var self = this;

    options = _.extend( {
      barHeight: 50,
      xMargin: 10,
      yMargin: 8,
      barFill: 'lightGray',
      barStroke: null,

      // true: float bar to top of visible bounds; false: bar at top of layoutBounds
      floatToTop: false,

      // true: keeps things on the status bar aligned with left and right edges of window bounds
      // false: align things on status bar with left and right edges of static layoutBounds
      dynamicAlignment: true

    }, options );

    // @private
    this.layoutBounds = layoutBounds;
    this.xMargin = options.xMargin;
    this.yMargin = options.yMargin;
    this.dynamicAlignment = options.dynamicAlignment;

    // @private size will be set by visibleBoundsListener
    this.barNode = new Rectangle( {
      fill: options.barFill,
      stroke: options.barStroke
    } );

    // Support decoration, with the bar behind everything else
    options.children = [ this.barNode ].concat( options.children || [] );

    Node.call( this, options );

    var visibleBoundsListener = function( visibleBounds ) {

      // resize the bar
      var y = ( options.floatToTop ) ? visibleBounds.top : layoutBounds.top;
      self.barNode.setRect( visibleBounds.minX, y, visibleBounds.width, options.barHeight );

      // update layout of things on the bar
      self.updateLayout();
    };
    visibleBoundsProperty.link( visibleBoundsListener );

    // @private
    this.disposeStatusBar = function() {
      if ( visibleBoundsProperty.hasListener( visibleBoundsListener ) ) {
        visibleBoundsProperty.unlink( visibleBoundsListener );
      }
    };
  }
Example #4
0
  /**
   * @param {LineGameModel} model
   * @param {Bounds2} layoutBounds
   * @param {Property.<Bounds2>} visibleBoundsProperty
   * @param {GameAudioPlayer} audioPlayer
   * @constructor
   */
  function PlayNode( model, layoutBounds, visibleBoundsProperty, audioPlayer ) {

    Node.call( this );

    var statusBar = new FiniteStatusBar( layoutBounds, visibleBoundsProperty, model.scoreProperty, {
      scoreDisplayConstructor: ScoreDisplayLabeledNumber,

      // FiniteStatusBar uses 1-based level numbering, model is 0-based, see #88.
      levelProperty: new DerivedProperty( [ model.levelProperty ], function( level ) { return level + 1; } ),
      challengeIndexProperty: model.challengeIndexProperty,
      numberOfChallengesProperty: model.challengesPerGameProperty,
      elapsedTimeProperty: model.timer.elapsedTimeProperty,
      timerEnabledProperty: model.timerEnabledProperty,
      font: new GLFont( 20 ),
      textFill: 'white',
      barFill: 'rgb( 49, 117, 202 )',
      xMargin: 40,
      startOverButtonOptions: {
        baseColor: 'rgb( 229, 243, 255 )',
        textFill: 'black',
        xMargin: 10,
        yMargin: 5,
        listener: function() {
          model.setGamePhase( GamePhase.SETTINGS );
        }
      }
    } );
    this.addChild( statusBar );

    // compute the size of the area available for the challenges
    var challengeSize = new Dimension2( layoutBounds.width, layoutBounds.height - statusBar.bottom );

    // challenge parent, to keep challenge below scoreboard
    var challengeParent = new Rectangle( 0, 0, 0, 1 );
    challengeParent.top = statusBar.bottom;
    this.addChild( challengeParent );

    // Set up a new challenge
    // unlink unnecessary because PlayNode exists for the lifetime of the sim.
    model.challengeProperty.link( function( challenge ) {

      // dispose of view for previous challenge
      var challengeNodes = challengeParent.getChildren();
      for ( var i = 0; i < challengeNodes.length; i++ ) {
        var challengeNode = challengeNodes[ i ];
        assert && assert( challengeNode instanceof ChallengeNode );
        challengeParent.removeChild( challengeNode );
        challengeNode.dispose();
      }

      // add view for current challenge
      challengeParent.addChild( challenge.createView( model, challengeSize, audioPlayer ) );
    } );
  }
Example #5
0
  /**
   * Constructs an Image node from a particular source.
   * @public
   * @constructor
   * @extends Node
   *
   * IMAGE_OPTION_KEYS (above) describes the available options keys that can be provided, on top of Node's options.
   *
   * @param {string|HTMLImageElement|HTMLCanvasElement|Array} image - See setImage() for details.
   * @param {Object} [options] - Image-specific options are documented in IMAGE_OPTION_KEYS above, and can be provided
   *                             along-side options for Node
   */
  function Image( image, options ) {
    assert && assert( image, 'image should be available' );
    assert && assert( options === undefined || Object.getPrototypeOf( options ) === Object.prototype,
      'Extra prototype on Node options object is a code smell' );

    // @private {number} - Internal stateful value, see setInitialWidth() for documentation.
    this._initialWidth = DEFAULT_OPTIONS.initialWidth;

    // @private {number} - Internal stateful value, see setInitialHeight() for documentation.
    this._initialHeight = DEFAULT_OPTIONS.initialHeight;

    // @private {number} - Internal stateful value, see setImageOpacity() for documentation.
    this._imageOpacity = DEFAULT_OPTIONS.imageOpacity;

    // @private {boolean} - Internal stateful value, see setMipmap() for documentation.
    this._mipmap = DEFAULT_OPTIONS.mipmap;

    // @private {number} - Internal stateful value, see setMipmapBias() for documentation.
    this._mipmapBias = DEFAULT_OPTIONS.mipmapBias;

    // @private {number} - Internal stateful value, see setMipmapInitialLevel() for documentation.
    this._mipmapInitialLevel = DEFAULT_OPTIONS.mipmapInitialLevel;

    // @private {number} - Internal stateful value, see setMipmapMaxLevel() for documentation
    this._mipmapMaxLevel = DEFAULT_OPTIONS.mipmapMaxLevel;

    // @private {Array.<HTMLCanvasElement>} - Array of Canvases for each level, constructed internally so that
    //                                        Canvas-based drawables (Canvas, WebGL) can quickly draw mipmaps.
    this._mipmapCanvases = [];

    // @private {Array.<String>} - Array of URLs for each level, where each URL will display an image (and is typically
    //                             a data URI or blob URI), so that we can handle mipmaps in SVG where URLs are
    //                             required.
    this._mipmapURLs = [];

    // @private {Array|null} - Mipmap data if it is passed into our image. Will be stored here for processing
    this._mipmapData = null;

    // @private {function} - Listener for invalidating our bounds whenever an image is invalidated.
    this._imageLoadListener = this.onImageLoad.bind( this );

    // @private {boolean} - Whether our _imageLoadListener has been attached as a listener to the current image.
    this._imageLoadListenerAttached = false;

    // rely on the setImage call from the super constructor to do the setup
    options = extendDefined( {
      image: image
    }, options );

    Node.call( this, options );

    this.invalidateSupportedRenderers();
  }
Example #6
0
 /**
  *
  * @param {Vector2} centroid the location the ticks radiate from (but do not touch)
  * @param {Object} [options]
  * @constructor
  */
 function TickMarksNode( centroid, options ) {
   Node.call( this );
   var totalAngleToSubtend = 60 * Math.PI / 180;//60 degrees in radians
   var tickSpacing = totalAngleToSubtend / 6;
   for ( var i = 0; i <= 6; i++ ) {
     var angle = -i * tickSpacing - Math.PI / 2;
     var startDistance = 110;
     var tickLength = (i === 0 || i === 6) ? 16 : 10;
     var lineWidth = (i === 0 || i === 6) ? 1.5 : 1;
     var pt1 = Vector2.createPolar( startDistance, angle ).plus( centroid );
     var pt2 = Vector2.createPolar( startDistance + tickLength, angle ).plus( centroid );
     var line = new Line( pt1.x, pt1.y, pt2.x, pt2.y, { stroke: 'white', lineWidth: lineWidth } );
     this.addChild( line );
   }
   this.mutate( options );
 }
  /**
   * @constructor
   *
   * @param {Property.<number>} frictionProperty - Property to update by slider.
   * @param {Range} frictionRange - Possible range of frictionProperty value.
   * @param {Object} [options]
   */
  function FrictionSliderNode( frictionProperty, frictionRange, options ) {

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

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

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

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

    // describes the panel box containing the friction slider
    Node.call( this, _.extend( {
      children: [ numberControl ]
    }, options ) );
  }
  /**
   * @constructor
   * @param {BASEModel} model
   * @param {BalloonModel} balloonModel
   * @param {BalloonNode} balloonNode 
   * @param {Bounds2} layoutBounds
   */
  function BalloonInteractionCueNode( model, balloonModel, balloonNode, layoutBounds ) {

    Node.call( this );

    // create the help node for the WASD and arrow keys, invisible except for on the initial balloon pick up
    var directionKeysParent = new Node();
    this.addChild( directionKeysParent );

    var wNode = this.createMovementKeyNode( 'up' );
    var aNode = this.createMovementKeyNode( 'left' );
    var sNode = this.createMovementKeyNode( 'down' );
    var dNode = this.createMovementKeyNode( 'right' );

    directionKeysParent.addChild( wNode );
    directionKeysParent.addChild( aNode );
    directionKeysParent.addChild( sNode );
    directionKeysParent.addChild( dNode );

    // add listeners to update visibility of nodes when location changes and when the wall is made
    // visible/invisible
    Property.multilink( [ balloonModel.locationProperty, model.wall.isVisibleProperty ], function( location, visible ) {

      // get the max x locations depending on if the wall is visible
      var centerXRightBoundary;
      if ( visible ) {
        centerXRightBoundary = PlayAreaMap.X_LOCATIONS.AT_WALL;
      }
      else {
        centerXRightBoundary = PlayAreaMap.X_BOUNDARY_LOCATIONS.AT_RIGHT_EDGE;
      }

      var balloonCenter = balloonModel.getCenter();
      aNode.visible = balloonCenter.x !== PlayAreaMap.X_BOUNDARY_LOCATIONS.AT_LEFT_EDGE;
      sNode.visible = balloonCenter.y !== PlayAreaMap.Y_BOUNDARY_LOCATIONS.AT_BOTTOM;
      dNode.visible = balloonCenter.x !== centerXRightBoundary;
      wNode.visible = balloonCenter.y !== PlayAreaMap.Y_BOUNDARY_LOCATIONS.AT_TOP;
    } );

    // place the direction cues relative to the balloon bounds
    var balloonBounds = balloonModel.bounds;
    wNode.centerBottom = balloonBounds.getCenterTop().plusXY( 0, -BALLOON_KEY_SPACING );
    aNode.rightCenter = balloonBounds.getLeftCenter().plusXY( -BALLOON_KEY_SPACING, 0 );
    sNode.centerTop = balloonBounds.getCenterBottom().plusXY( 0, BALLOON_KEY_SPACING + SHADOW_WIDTH );
    dNode.leftCenter = balloonBounds.getRightCenter().plusXY( BALLOON_KEY_SPACING + SHADOW_WIDTH, 0 );
  }
  var TIMER_DELAY = 100; // In milliseconds.

  /**
   *
   * @param {string} text
   * @param {boolean} initiallyVisible
   * @param {Property<number>} existenceStrengthProperty
   * @constructor
   */
  function FadeLabel( text, initiallyVisible, existenceStrengthProperty ) {
    var self = this;
    Node.call( self, { pickable: false } );
    this.fadeDelta = 0; // @private
    var opacity = 0;

    var label = new Text( text, { font: FONT, maxWidth: 80 } );
    this.addChild( label );

    if ( !initiallyVisible ) {
      this.setOpacity( 0 );
      opacity = 0;
    }
    else {
      opacity = 1;
    }

    // Create the timers that will be used for fading in and out.
    this.fadeInTimer = new FadeTimer( TIMER_DELAY, function() {
      opacity = Math.min( opacity + self.fadeDelta, existenceStrengthProperty.get() );
      updateTransparency();
      if ( opacity >= 1 ) {
        self.fadeInTimer.stop();
      }
    } );

    this.fadeOutTimer = new FadeTimer( TIMER_DELAY, function() {
      opacity = Math.min( Math.max( opacity - self.fadeDelta, 0 ), existenceStrengthProperty.get() );
      updateTransparency();
      if ( opacity <= 0 ) {
        self.fadeOutTimer.stop();
      }
    } );

    function updateTransparency() {
      self.opacity = Math.min( existenceStrengthProperty.get(), opacity );
    }

    // Update if existence strength changes.
    existenceStrengthProperty.link( function() {
      updateTransparency();
    } );

  }
  function ChamberPoolView( model, mvt, dragBounds ) {
    var self = this;

    Node.call( this, { renderer: 'svg' } );

    //pool
    this.addChild( new ChamberPoolBack( model, mvt ) );

    //water
    this.addChild( new ChamberPoolWaterNode( model, mvt ) );

    model.masses.forEach( function( massModel ) {
      self.addChild( new MassViewNode( massModel, model, mvt, dragBounds ) );
    } );

    this.addChild( new MassStackNode( model, mvt ) );

    //grid
    this.addChild( new ChamberPoolGrid( model, mvt ) );
  }
Example #11
0
  //-------------------------------------------------------------------------------------

  /**
   * @param {Beaker} beaker
   * @param {Solution} solution
   * @param {ModelViewTransform2} modelViewTransform
   * @param {*} options
   * @constructor
   */
  function RatioNode( beaker, solution, modelViewTransform, options ) {

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

    // save constructor args
    thisNode.solution = solution; // @private

    // current pH
    thisNode.pH = null; // @private null to force an update

    // bounds of the beaker, in view coordinates
    var beakerBounds = modelViewTransform.modelToViewBounds( beaker.bounds );

    // parent for all molecules
    thisNode.moleculesNode = new MoleculesCanvas( beakerBounds ); // @private
    thisNode.addChild( thisNode.moleculesNode );

    // dev mode, show numbers of molecules at bottom of beaker
    if ( window.phetcommon.getQueryParameter( 'dev' ) ) {
      thisNode.ratioText = new Text( '?', { font: new PhetFont( 30 ), fill: 'black', left: beakerBounds.getCenterX(), bottom: beakerBounds.maxY - 20 } ); // @private
      thisNode.addChild( thisNode.ratioText );
    }

    thisNode.mutate( options ); // call before registering for property notifications, because 'visible' significantly affects initialization time

    // sync view with model
    solution.pHProperty.link( thisNode.update.bind( thisNode ) );

    // clip to the shape of the solution in the beaker
    solution.volumeProperty.link( function( volume ) {
      if ( volume === 0 ) {
        thisNode.clipArea = null;
      }
      else {
        var solutionHeight = beakerBounds.getHeight() * volume / beaker.volume;
        thisNode.clipArea = Shape.rectangle( beakerBounds.minX, beakerBounds.maxY - solutionHeight, beakerBounds.getWidth(), solutionHeight );
      }
      thisNode.moleculesNode.invalidatePaint(); //WORKAROUND: #25, scenery#200
    } );
  }
Example #12
0
  /**
   * Constructor for the AxonBodyNode
   * @param {NeuronModel} axonMembraneModel
   * @param {Bounds2} canvasBounds - bounds of the canvas for portraying the action potential, must be large enough
   * to not get cut off when view is at max zoom out
   * @param {ModelViewTransform2} mvt
   * @constructor
   */
  function AxonBodyNode( axonMembraneModel, canvasBounds, mvt ) {

    Node.call( this, {} );
    this.axonMembraneModel = axonMembraneModel;
    this.mvt = mvt;

    // Add the axon body.
    var axonBodyShape = this.mvt.modelToViewShape( axonMembraneModel.axonBodyShape );
    var axonBodyBounds = axonBodyShape.bounds;
    var gradientOrigin = new Vector2( axonBodyBounds.getMaxX(), axonBodyBounds.getMaxY() );
    var gradientExtent = new Vector2( mvt.modelToViewX( axonMembraneModel.crossSectionCircleCenter.x ),
      mvt.modelToViewDeltaX( axonMembraneModel.crossSectionCircleRadius ) );
    var axonBodyGradient = new LinearGradient( gradientOrigin.x, gradientOrigin.y, gradientExtent.x, gradientExtent.y );
    axonBodyGradient.addColorStop( 0, AXON_BODY_COLOR.darkerColor( 0.5 ) );
    axonBodyGradient.addColorStop( 1, AXON_BODY_COLOR.brighterColor( 0.5 ) );

    var axonBody = new Path( axonBodyShape, {
      fill: axonBodyGradient,
      stroke: 'black',
      lineWidth: LINE_WIDTH
    } );
    this.addChild( axonBody );

    if ( SHOW_GRADIENT_LINE ) {
      // The following line is useful when trying to debug the gradient.
      this.addChild( new Line( gradientOrigin, gradientExtent ) );
    }

    var travelingActionPotentialNode = new TravelingActionPotentialCanvasNode( this.mvt, canvasBounds );
    this.addChild( travelingActionPotentialNode );

    this.axonMembraneModel.travelingActionPotentialStarted.addListener( function() {
      travelingActionPotentialNode.travelingActionPotentialStarted( axonMembraneModel.travelingActionPotential );
    } );

    this.axonMembraneModel.travelingActionPotentialEnded.addListener( function() {
        travelingActionPotentialNode.travelingActionPotentialEnded();
    } );
  }
  /**
   * @param {Property.<Equation>} equationProperty the equation displayed in the boxes
   * @param {Range} coefficientsRange
   * @param {HorizontalAligner} aligner provides layout information to ensure horizontal alignment with other user-interface elements
   * @param {Dimension2} boxSize
   * @param {string} boxColor fill color of the boxes
   * @param {Property.<boolean>} reactantsBoxExpandedProperty
   * @param {Property.<boolean>} productsBoxExpandedProperty
   * @param {Object} [options]
   * @constructor
   */
  function BoxesNode( equationProperty, coefficientsRange, aligner, boxSize, boxColor,
                      reactantsBoxExpandedProperty, productsBoxExpandedProperty, options ) {

    // reactants box, on the left
    var reactantsBoxNode = new BoxNode( equationProperty,
      function( equation ) { return equation.reactants; },
      function( equation ) { return aligner.getReactantXOffsets( equation ); },
      coefficientsRange,
      reactantsString, {
        expandedProperty: reactantsBoxExpandedProperty,
        fill: boxColor,
        boxWidth: boxSize.width,
        boxHeight: boxSize.height,
        x: aligner.getReactantsBoxLeft(),
        y: 0
      } );

    // products box, on the right
    var productsBoxNode = new BoxNode( equationProperty,
      function( equation ) { return equation.products; },
      function( equation ) { return aligner.getProductXOffsets( equation ); },
      coefficientsRange,
      productsString, {
        expandedProperty: productsBoxExpandedProperty,
        fill: boxColor,
        boxWidth: boxSize.width,
        boxHeight: boxSize.height,
        x: aligner.getProductsBoxLeft(),
        y: 0
      } );

    // @private right-pointing arrow, in the middle
    this.arrowNode = new RightArrowNode( equationProperty,
      { center: new Vector2( aligner.getScreenCenterX(), boxSize.height / 2 ) } );

    options.children = [ reactantsBoxNode, productsBoxNode, this.arrowNode ];
    Node.call( this, options );
  }
  /**
   * @param {TrapezoidPoolModel} trapezoidPoolModel
   * @param {ModelViewTransform2 } modelViewTransform to convert between model and view co-ordinates
   * @constructor
   */
  function TrapezoidPoolView( trapezoidPoolModel, modelViewTransform ) {

    Node.call( this );
    var poolDimensions = trapezoidPoolModel.poolDimensions;

    // add pool back
    this.addChild( new TrapezoidPoolBack( trapezoidPoolModel, modelViewTransform ) );

    // add fluids
    var inputFaucetFluidMaxHeight = Math.abs( modelViewTransform.modelToViewDeltaY( trapezoidPoolModel.inputFaucet.location.y -
                                                                                    poolDimensions.bottomChamber.y2 ) );
    this.addChild( new FaucetFluidNode(
      trapezoidPoolModel.inputFaucet, trapezoidPoolModel, modelViewTransform, inputFaucetFluidMaxHeight ) );

    var outputFaucetFluidMaxHeight = 1000;
    this.addChild( new FaucetFluidNode( trapezoidPoolModel.outputFaucet, trapezoidPoolModel, modelViewTransform,
      outputFaucetFluidMaxHeight ) );

    // add water
    this.addChild( new TrapezoidPoolWaterNode( trapezoidPoolModel, modelViewTransform ) );

    // pool dimensions in view values
    var poolLeftX = poolDimensions.leftChamber.centerTop - poolDimensions.leftChamber.widthBottom / 2;
    var poolTopY = poolDimensions.leftChamber.y;
    var poolRightX = poolDimensions.rightChamber.centerTop + poolDimensions.rightChamber.widthTop / 2;
    var poolBottomY = poolDimensions.leftChamber.y - poolDimensions.leftChamber.height - 0.3;
    var poolHeight = poolDimensions.leftChamber.height;

    var labelXPosition = modelViewTransform.modelToViewX(
      ( poolDimensions.leftChamber.centerTop + poolDimensions.leftChamber.widthTop / 2 +
        poolDimensions.rightChamber.centerTop - poolDimensions.rightChamber.widthTop / 2 ) / 2 );

    var slantMultiplier = 0.45; // Empirically determined to make labels line up in space between the pools

    // add grid
    this.addChild( new TrapezoidPoolGrid( trapezoidPoolModel.underPressureModel, modelViewTransform, poolLeftX,
      poolTopY, poolRightX, poolBottomY, poolHeight, labelXPosition, slantMultiplier ) );
  }
  var SYMBOL_ASPECT_RATIO = 1.0; // Width/height.

  /**
   * @param numberAtom
   * @constructor
   */
  function PeriodicTableAndSymbol( numberAtom ) {

    Node.call( this ); // Call super constructor.

    // Create and add the periodic table.
    var periodicTable = new PeriodicTableNode( numberAtom, 0 );
    this.addChild( periodicTable );

    // Create and add the symbol, which only shows a bigger version of the selected element symbol.
    var symbolRectangle = new Rectangle( 0, 0, periodicTable.width * SYMBOL_WIDTH_PROPORTION, periodicTable.width * SYMBOL_WIDTH_PROPORTION / SYMBOL_ASPECT_RATIO,
      {
        fill: 'white',
        stroke: 'black',
        lineWidth: 2
      } );
    this.addChild( symbolRectangle );

    // Add the text that represents the chosen element.
    numberAtom.protonCountProperty.link( function() {
      symbolRectangle.removeAllChildren();
      var symbolText = new Text( AtomIdentifier.getSymbol( numberAtom.protonCount ),
        {
          font: new PhetFont( { size: 48, weight: 'bold' } )
        } );
      symbolText.scale( Math.min( Math.min( symbolRectangle.width * 0.8 / symbolText.width, symbolRectangle.height * 0.8 / symbolText.height ), 1 ) );
      symbolText.center = new Vector2( symbolRectangle.width / 2, symbolRectangle.height / 2 );
      symbolRectangle.addChild( symbolText );
    } );

    // Do the layout.  This positions the symbol to fit into the top portion
    // of the table.  The periodic table is 18 cells wide, and this needs
    // to be centered over the 8th column to be in the right place.
    symbolRectangle.centerX = (7.5 / 18 ) * periodicTable.width;
    symbolRectangle.top = 0;
    periodicTable.top = symbolRectangle.bottom - ( periodicTable.height / 7 * 2.5);
    periodicTable.left = 0;
  }
Example #16
0
  /**
   * @constructor
   *
   * @param {PaperNumber} paperNumber
   * @param {Property.<Bounds2>} availableViewBoundsProperty
   * @param {Function} addAndDragNumber - function( event, paperNumber ), adds and starts a drag for a number
   * @param {Function} tryToCombineNumbers - function( paperNumber ), called to combine our paper number
   */
  function PaperNumberNode( paperNumber, availableViewBoundsProperty, addAndDragNumber, tryToCombineNumbers ) {
    var self = this;

    Node.call( this );

    // @public {PaperNumber} - Our model
    this.paperNumber = paperNumber;

    // @public {Emitter} - Triggered with self when this paper number node starts to get dragged
    this.moveEmitter = new Emitter( { validators: [ { valueType: PaperNumberNode } ] } );

    // @public {Emitter} - Triggered with self when this paper number node is split
    this.splitEmitter = new Emitter( { validators: [ { valueType: PaperNumberNode } ] } );

    // @public {Emitter} - Triggered when user interaction with this paper number begins.
    this.interactionStartedEmitter = new Emitter( { validators: [ { valueType: PaperNumberNode } ] } );

    // @private {boolean} - When true, don't emit from the moveEmitter (synthetic drag)
    this.preventMoveEmit = false;

    // @private {Bounds2}
    this.availableViewBoundsProperty = availableViewBoundsProperty;

    // @private {Node} - Container for the digit image nodes
    this.numberImageContainer = new Node( {
      pickable: false
    } );
    this.addChild( this.numberImageContainer );

    // @private {Rectangle} - Hit target for the "split" behavior, where one number would be pulled off from the
    //                        existing number.
    this.splitTarget = new Rectangle( 0, 0, 100, 100, {
      cursor: 'pointer'
    } );
    this.addChild( this.splitTarget );

    // @private {Rectangle} - Hit target for the "move" behavior, which just drags the existing paper number.
    this.moveTarget = new Rectangle( 0, 0, 100, 100, {
      cursor: 'move'
    } );
    this.addChild( this.moveTarget );

    // View-coordinate offset between our position and the pointer's position, used for keeping drags synced.
    // @private {DragListener}
    this.moveDragHandler = new DragListener( {
      targetNode: this,
      start: function( event, listener ) {
        self.interactionStartedEmitter.emit( self );
        if ( !self.preventMoveEmit ) {
          self.moveEmitter.emit( self );
        }
      },

      drag: function( event, listener ) {
        paperNumber.setConstrainedDestination( availableViewBoundsProperty.value, listener.parentPoint );
      },

      end: function( event, listener ) {
        tryToCombineNumbers( self.paperNumber );
        paperNumber.endDragEmitter.emit( paperNumber );
      }
    } );
    this.moveDragHandler.isUserControlledProperty.link( function( controlled ) {
      paperNumber.userControlledProperty.value = controlled;
    } );
    this.moveTarget.addInputListener( this.moveDragHandler );

    // @private {Object}
    this.splitDragHandler = {
      down: function( event ) {
        if ( !event.canStartPress() ) { return; }

        var viewPosition = self.globalToParentPoint( event.pointer.point );

        // Determine how much (if any) gets moved off
        var pulledPlace = paperNumber.getBaseNumberAt( self.parentToLocalPoint( viewPosition ) ).place;
        var amountToRemove = ArithmeticRules.pullApartNumbers( paperNumber.numberValueProperty.value, pulledPlace );
        var amountRemaining = paperNumber.numberValueProperty.value - amountToRemove;

        // it cannot be split - so start moving
        if ( !amountToRemove ) {
          self.startSyntheticDrag( event );
          return;
        }

        paperNumber.changeNumber( amountRemaining );

        self.interactionStartedEmitter.emit( self );
        self.splitEmitter.emit( self );

        var newPaperNumber = new PaperNumber( amountToRemove, paperNumber.positionProperty.value );
        addAndDragNumber( event, newPaperNumber );
      }
    };
    this.splitTarget.addInputListener( this.splitDragHandler );

    // @private {Function} - Listener that hooks model position to view translation.
    this.translationListener = function( position ) {
      self.translation = position;
    };

    // @private {Function} - Listener for when our number changes
    this.updateNumberListener = this.updateNumber.bind( this );

    // @private {Function} - Listener reference that gets attached/detached. Handles moving the Node to the front.
    this.userControlledListener = function( userControlled ) {
      if ( userControlled ) {
        self.moveToFront();
      }
    };
  }
Example #17
0
  var BULB_X_DISPLACEMENT = -45; // Bulb dx relative to center position

  /**
   *
   * @param needleAngleProperty - value of voltage meter.
   * @param options
   * @constructor
   */
  function BulbNode( needleAngleProperty, options ) {
    Node.call( this );

    // Create the base of the bulb
    var bulbBase = new Image( bulbBaseImage );
    bulbBase.scale( BULB_BASE_WIDTH / bulbBase.bounds.height );

    // Important Note: For the drawing code below, the reference frame is assumed to be such that the point x=0, y=0 is
    // at the left side of the light bulb base, which is also the right side of the light bulb body, and the vertical
    // center of both.  This was the easiest to work with.

    // Create the bulb body.
    var bulbNeckWidth = BULB_BASE_WIDTH * 0.85;
    var bulbBodyHeight = BULB_HEIGHT - bulbBase.bounds.width;
    var controlPointYValue = BULB_WIDTH * 0.7;
    var bulbShape = new Shape().
      moveTo( 0, -bulbNeckWidth / 2 ).
      cubicCurveTo( -bulbBodyHeight * 0.33, -controlPointYValue, -bulbBodyHeight * 0.95, -controlPointYValue, -bulbBodyHeight, 0 ).
      cubicCurveTo( -bulbBodyHeight * 0.95, controlPointYValue, -bulbBodyHeight * 0.33, controlPointYValue, 0, bulbNeckWidth / 2 );
    var bulbBodyOutline = new Path( bulbShape, {
      stroke: 'black',
      lineCap: 'round'
    } );
    var bulbBodyFill = new Path( bulbShape, {
      fill: new RadialGradient( bulbBodyOutline.centerX, bulbBodyOutline.centerY, BULB_WIDTH / 10, bulbBodyOutline.centerX,
        bulbBodyOutline.centerY, BULB_WIDTH / 2 ).addColorStop( 0, '#eeeeee' ).addColorStop( 1, '#bbccbb' )
    } );

    // Create the filament support wires.
    var filamentWireHeight = bulbBodyHeight * 0.6;
    var filamentTopPoint = new Vector2( -filamentWireHeight, -BULB_WIDTH * 0.3 );
    var filamentBottomPoint = new Vector2( -filamentWireHeight, BULB_WIDTH * 0.3 );
    var filamentSupportWiresShape = new Shape();
    filamentSupportWiresShape.moveTo( 0, -BULB_BASE_WIDTH * 0.3 );
    filamentSupportWiresShape.cubicCurveTo( -filamentWireHeight * 0.3, -BULB_BASE_WIDTH * 0.3, -filamentWireHeight * 0.4, filamentTopPoint.y, filamentTopPoint.x, filamentTopPoint.y );
    filamentSupportWiresShape.moveTo( 0, BULB_BASE_WIDTH * 0.3 );
    filamentSupportWiresShape.cubicCurveTo( -filamentWireHeight * 0.3, BULB_BASE_WIDTH * 0.3, -filamentWireHeight * 0.4, filamentBottomPoint.y, filamentBottomPoint.x, filamentBottomPoint.y );
    var filamentSupportWires = new Path( filamentSupportWiresShape, { stroke: 'black' } );

    // Create the filament, which is a zig-zag shape.
    var filamentShape = new Shape().moveToPoint( filamentTopPoint );
    for ( var i = 0; i < NUM_FILAMENT_ZIG_ZAGS - 1; i++ ) {
      var yPos = filamentTopPoint.y + ( filamentBottomPoint.y - filamentTopPoint.y ) / NUM_FILAMENT_ZIG_ZAGS * (i + 1);
      if ( i % 2 === 0 ) {
        // zig
        filamentShape.lineTo( filamentTopPoint.x + FILAMENT_ZIG_ZAG_SPAN, yPos );
      }
      else {
        // zag
        filamentShape.lineTo( filamentTopPoint.x, yPos );
      }
    }
    filamentShape.lineToPoint( filamentBottomPoint );
    var filament = new Path( filamentShape, { stroke: 'black' } );

    // Create the 'halo' that makes the bulb look like it is shining.
    var haloNode = new Node();
    haloNode.addChild( new Circle( 5, {
      fill: 'white',
      opacity: 0.46
    } ) );
    haloNode.addChild( new Circle( 3.75, {
      fill: 'white',
      opacity: 0.51
    } ) );
    haloNode.addChild( new Circle( 2, {
      fill: 'white'
    } ) );

    // Update the halo as the needle angle changes.
    needleAngleProperty.link( function( angle ) {
      var targetScaleFactor = 20 * Math.abs( angle ); //from flash simulation, in angle = 1, we would have 200x200 halo (max circle diameter - 10px, so 200/10 = 20)
      if ( targetScaleFactor < 0.1 ) {
        haloNode.visible = false;
      }
      else {
        haloNode.visible = true;
        var scale = targetScaleFactor / haloNode.transform.matrix.scaleVector.x;
        haloNode.scale( scale );
      }
    } );

    // Add the children in the order needed to get the desired layering
    this.addChild( bulbBodyFill );
    this.addChild( filamentSupportWires );
    this.addChild( filament );
    this.addChild( haloNode );
    this.addChild( bulbBase );
    this.addChild( bulbBodyOutline );

    // Do some last layout
    bulbBase.centerY = 0;
    bulbBase.left = 0;
    haloNode.center = filament.center;

    this.mutate( options );

    this.centerX = this.centerX + BULB_X_DISPLACEMENT;
  }
  var CollectionPanel = namespace.CollectionPanel = function CollectionPanel( collectionList, isSingleCollectionMode, collectionAttachmentCallbacks, toModelBounds ) {
    var panel = this;
    Node.call( this, {} );

    var y = 0; // TODO: improve layout code using GeneralLayoutNode?

    this.layoutNode = new Node();
    this.collectionAreaHolder = new Node();
    this.backgroundHolder = new Node();
    this.collectionAreaMap = {}; // kitCollection id => node
    this.collectionAttachmentCallbacks = collectionAttachmentCallbacks;

    // move it over so the background will have padding
    this.layoutNode.setTranslation( containerPadding, containerPadding );

    // "Your Molecule Collection"
    var moleculeCollectionText = new Text( collection_yourMoleculeCollectionString, {
      font: new PhetFont( {
        size: 22
      } )
    } );
    this.layoutNode.addChild( moleculeCollectionText );
    moleculeCollectionText.top = 0;
    y += moleculeCollectionText.height + 5;

    // "Collection X" with arrows
    var currentCollectionText = new Text( '', {
      font: new PhetFont( {
        size: 16,
        weight: 'bold'
      } )
    } );
    collectionList.currentCollectionProperty.link( function() {
      currentCollectionText.text = StringUtils.format( collection_labelString, collectionList.currentIndex + 1 );
    } );
    var collectionSwitcher = new NextPreviousNavigationNode( currentCollectionText, {
      arrowColor: Constants.kitArrowBackgroundEnabled,
      arrowStrokeColor: Constants.kitArrowBorderEnabled,
      arrowWidth: 14,
      arrowHeight: 18,
      next: function() {
        collectionList.switchToNextCollection();
      },
      previous: function() {
        collectionList.switchToPreviousCollection();
      },
      touchAreaExtension: function( shape ) {
        // square touch area
        return Shape.bounds( shape.bounds.dilated( 7 ) );
      }
    } );

    function updateSwitcher() {
      collectionSwitcher.hasNext = collectionList.hasNextCollection();
      collectionSwitcher.hasPrevious = collectionList.hasPreviousCollection();
    }

    collectionList.currentCollectionProperty.link( updateSwitcher );
    collectionList.on( 'addedCollection', updateSwitcher );
    collectionList.on( 'removedCollection', updateSwitcher );
    this.layoutNode.addChild( collectionSwitcher );
    collectionSwitcher.top = y;
    y += collectionSwitcher.height + 10;

    // all of the collection boxes themselves
    this.layoutNode.addChild( this.collectionAreaHolder );
    this.collectionAreaHolder.y = y;
    y += 5; // TODO: height?

    // sound on/off
    this.soundToggleButton = new SoundToggleButton( namespace.soundEnabled );
    this.soundToggleButton.touchArea = Shape.bounds( this.soundToggleButton.bounds.dilated( 7 ) );
    this.layoutNode.addChild( this.soundToggleButton );
    this.soundToggleButton.top = y;

    // add our two layers: background and controls
    this.addChild( this.backgroundHolder );
    this.addChild( this.layoutNode );

    // anonymous function here, so we don't create a bunch of fields
    function createCollectionNode( collection ) {
      panel.collectionAreaMap[collection.id] = new CollectionAreaNode( collection, isSingleCollectionMode, toModelBounds );
    }

    // create nodes for all current collections
    _.each( collectionList.collections, function( collection ) {
      createCollectionNode( collection );
    } );

    // if a new collection is added, create one for it
    collectionList.on( 'addedCollection', function( collection ) {
      createCollectionNode( collection );
    } );

    // use the current collection
    this.useCollection( collectionList.currentCollection );

    collectionList.currentCollectionProperty.link( function( newCollection ) {
      panel.useCollection( newCollection );
    } );
  };
Example #19
0
  var DROP_BOUNDS_HEIGHT_PROPORTION = 0.35; // the bounds proportion within which if user drops a number we can consider collapsing them

  /**
   *
   * @param {PaperNumberModel} paperNumberModel
   * @param {Function<paperNumberModel>} addNumberModelCallBack A callback to invoke when a  Number is  split
   * @param {Function<paperNumberModel,droppedPoint>} combineNumbersIfApplicableCallback A callback to invoke when a Number is  combined
   * @constructor
   */
  function PaperNumberNode( paperNumberModel, addNumberModelCallBack, combineNumbersIfApplicableCallback ) {
    var thisNode = this;
    thisNode.paperNumberModel = paperNumberModel;
    Node.call( thisNode );

    thisNode.addNumberModelCallBack = addNumberModelCallBack || _.noop();
    combineNumbersIfApplicableCallback = combineNumbersIfApplicableCallback || _.noop();

    var imageNumberNode = new Node();
    thisNode.addChild( imageNumberNode );

    paperNumberModel.numberValueProperty.link( function( newNumber ) {
      imageNumberNode.removeAllChildren();
      _.each( paperNumberModel.baseImages, function( imageNode ) {
        imageNumberNode.addChild( imageNode );
      } );
    } );

    paperNumberModel.positionProperty.link( function( newPos ) {
      thisNode.leftTop = newPos;
    } );


    paperNumberModel.opacityProperty.link( function( opacity ) {
      imageNumberNode.opacity = opacity;
    } );

    var paperNodeDragHandler = new SimpleDragHandler( {

      // Allow moving a finger (touch) across this node to interact with it
      allowTouchSnag: true,

      movableObject: null,

      startOffSet: null,

      currentPoint: null,

      splitObjectContext: null,

      dragCursor: null,


      reset: function() {
        var thisHandler = this;
        thisHandler.startOffSet = null;
        thisHandler.currentPoint = null;
        thisHandler.splitObjectContext = null;
        thisHandler.movableObject = null;
      },

      startMoving: function( paperNumberModel ) {
        var thisHandler = this;
        thisHandler.movableObject = paperNumberModel;
        thisHandler.movableObject.userControlled = true;
      },

      start: function( event, trail ) {
        var thisHandler = this;
        thisHandler.reset();
        thisHandler.startOffSet = thisNode.globalToParentPoint( event.pointer.point );
        thisHandler.currentPoint = thisHandler.startOffSet.copy();

        if ( paperNumberModel.numberValue === 1 ) {
          this.startMoving( paperNumberModel );
          return;
        }

        var pulledOutIndex = thisNode.determineDigitIndex( thisHandler.startOffSet );
        var numberPulledApart = ArithmeticRules.pullApartNumbers( paperNumberModel.numberValue, pulledOutIndex );

        // it cannot be split - so start moving
        if ( !numberPulledApart ) {
          this.startMoving( paperNumberModel );
          return;
        }

        //check if split needs to happen
        var amountToRemove = numberPulledApart.amountToRemove;
        var amountRemaining = numberPulledApart.amountRemaining;

        // When splitting a single digit from a two, make sure the mouse is near that second digit (or third digit)
        // In the case of splitting equal digits (ex 30 splitting in to 20 and 10) we dont need to check this condition
        var removalOffsetPosition = thisNode.paperNumberModel.getDigitOffsetPosition( amountToRemove );
        var amountRemovingOffsetPosition = thisNode.paperNumberModel.getDigitOffsetPosition( amountRemaining );
        var totalBounds = thisNode.bounds;
        var splitRect = Bounds2.rect( totalBounds.x + removalOffsetPosition.x, totalBounds.y,
          totalBounds.width - removalOffsetPosition.x, totalBounds.height * SPLIT_MODE_HEIGHT_PROPORTION );

        //if the below condition is true, start splitting
        if ( splitRect.containsPoint( thisHandler.startOffSet ) ) {
          var pulledOutPosition = thisNode.determinePulledOutNumberPosition( amountToRemove );
          var pulledApartPaperNumberModel = new PaperNumberModel( amountToRemove, pulledOutPosition, {
            opacity: 0.95
          } );
          thisHandler.splitObjectContext = {};
          thisHandler.splitObjectContext.pulledApartPaperNumberModel = pulledApartPaperNumberModel;
          thisHandler.splitObjectContext.amountRemaining = amountRemaining;
          thisHandler.splitObjectContext.amountRemovingOffsetPosition = amountRemovingOffsetPosition;
          return;
        }

        // none matched, start moving
        this.startMoving( paperNumberModel );
        return;
      },

      // Handler that moves the shape in model space.
      translate: function( translationParams ) {
        var thisHandler = this;

        // How far it has moved from the original position
        var delta = translationParams.delta;
        thisHandler.currentPoint = thisHandler.currentPoint.plus( delta );
        var transDistance = thisHandler.currentPoint.distance( thisHandler.startOffSet );

        //if it is splitMode
        if ( thisHandler.splitObjectContext && transDistance > MIN_SPLIT_DISTANCE ) {
          thisNode.addNumberModelCallBack( thisHandler.splitObjectContext.pulledApartPaperNumberModel );
          paperNumberModel.changeNumber( thisHandler.splitObjectContext.amountRemaining );
          this.startMoving( thisHandler.splitObjectContext.pulledApartPaperNumberModel );

          // After a Number is pulled the  remainaing digits must stay in the same place.We use the amountRemovingOffsetPosition to adjust the new paperModel's position
          // see issue #7

          if ( thisHandler.splitObjectContext.pulledApartPaperNumberModel.getDigitLength() >= (thisHandler.splitObjectContext.amountRemaining + "").length ) {
            paperNumberModel.setDestination( paperNumberModel.position.plus( thisHandler.splitObjectContext.amountRemovingOffsetPosition ) );
          }
          if ( thisHandler.splitObjectContext.pulledApartPaperNumberModel.getDigitLength() > (thisHandler.splitObjectContext.amountRemaining + "").length ) {
            thisNode.moveToFront();
          }

          thisHandler.splitObjectContext = null;
        }

        //in case of split mode, the movableObject is set, only if the "move" started after a certain distance
        if ( thisHandler.movableObject ) {
          var movableObject = thisHandler.movableObject;
          movableObject.setDestination( movableObject.position.plus( delta ), false );
          // if it is a new created object, change the opacity
          if ( movableObject !== paperNumberModel ) {
            // gradually increase the opacity from 0.8 to 1 as we move away from the number, otherwise the change looks sudden
            movableObject.opacity = 0.9 + (0.005 * Math.min( 20, transDistance / SPLIT_OPACITY_FACTOR ));
          }
        }

        return translationParams.position;
      },

      end: function( event, trail ) {
        var thisHandler = this;
        var movableObject = thisHandler.movableObject;
        if ( movableObject ) {
          movableObject.userControlled = false;
          var droppedPoint = event.pointer.point;
          combineNumbersIfApplicableCallback( movableObject, droppedPoint );

          movableObject.trigger("endDrag");

        }

        thisHandler.reset();
      }

    } );

    thisNode.addInputListener( paperNodeDragHandler );

    // show proper cursor to differentiate move and split
    paperNodeDragHandler.move = function( event ) {

      // if it is 1, we can only move
      if ( paperNumberModel.numberValue === 1 ) {
        thisNode.cursor = 'move';
        return;
      }

      var localNodeBounds = thisNode.localBounds;
      var pullBounds = Bounds2.rect( localNodeBounds.x, localNodeBounds.y,
        localNodeBounds.width, localNodeBounds.height * SPLIT_MODE_HEIGHT_PROPORTION );

      var globalBounds = thisNode.localToGlobalBounds( pullBounds );
      if ( globalBounds.containsPoint( event.pointer.point ) ) {
        thisNode.cursor = 'pointer';
      }
      else {
        thisNode.cursor = 'move';
      }
    };

    paperNodeDragHandler.out = function( args ) {
      thisNode.cursor = 'default';
    };

  }
  /**
   * @param {MassModel} massModel of simulation
   * @param {ChamberPoolModel} chamberPoolModel
   * @param {ModelViewTransform2} modelViewTransform , Transform between model and view coordinate frames
   * @param {Bounds2} dragBounds - bounds that define where the node may be dragged
   * @constructor
   */
  function MassNode( massModel, chamberPoolModel, modelViewTransform, dragBounds ) {

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

    var width = modelViewTransform.modelToViewDeltaX( massModel.width );
    var height = Math.abs( modelViewTransform.modelToViewDeltaY( massModel.height ) );

    // add mass rectangle
    var mass = new Rectangle( -width / 2, -height / 2, width, height, {
      fill: new LinearGradient( -width / 2, 0, width, 0 )
        .addColorStop( 0, '#8C8D8D' )
        .addColorStop( 0.3, '#C0C1C2' )
        .addColorStop( 0.5, '#F0F1F1' )
        .addColorStop( 0.6, '#F8F8F7' ),
      stroke: '#918e8e',
      lineWidth: 1
    } );
    this.addChild( mass );

    var massText = new Text( StringUtils.format( massLabelPatternString, massModel.mass ),
      {
        //x: mass.centerX - 15,
        //y: mass.centerY + 3,
        font: new PhetFont( 9 ),
        fill: 'black',
        pickable: false,
        fontWeight: 'bold',
        maxWidth: width - 5
      } );
    this.addChild( massText );

    var massClickOffset = { x: 0, y: 0 };

    // mass drag handler
    this.addInputListener( new SimpleDragHandler( {
      //When dragging across it in a mobile device, pick it up
      allowTouchSnag: true,
      start: function( event ) {
        massClickOffset.x = self.globalToParentPoint( event.pointer.point ).x - event.currentTarget.x;
        massClickOffset.y = self.globalToParentPoint( event.pointer.point ).y - event.currentTarget.y;
        self.moveToFront();
        massModel.isDraggingProperty.value = true;
      },
      end: function() {
        massModel.positionProperty.value = modelViewTransform.viewToModelPosition( self.translation );
        massModel.isDraggingProperty.value = false;
      },
      //Translate on drag events
      drag: function( event ) {
        var point = self.globalToParentPoint( event.pointer.point ).subtract( massClickOffset );
        self.translation = dragBounds.getClosestPoint( point.x, point.y );
      }
    } ) );

    massModel.positionProperty.link( function( position ) {
      if ( !chamberPoolModel.isDragging ) {
        self.translation = new Vector2( modelViewTransform.modelToViewX( position.x ),
          modelViewTransform.modelToViewY( position.y ) );
        massText.centerX = mass.centerX;
        massText.centerY = mass.centerY;
      }
    } );
  }
  /**
   *
   * @param {FractionModel} leftFractionModel
   * @param {FractionModel} rightFractionModel
   * @param {Property.<boolean>} visibleProperty
   * @param {Object} [options]
   * @constructor
   */
  function NumberLineNode( leftFractionModel, rightFractionModel, visibleProperty, options ) {
    Node.call( this );

    var leftFractionProperty = leftFractionModel.fractionProperty;
    var rightFractionProperty = rightFractionModel.fractionProperty;

    var width = 300;
    var line = new Line( 0, 0, width, 0, { lineWidth: 2, stroke: 'black' } );

    this.addChild( line );

    var leftFill = '#61c9e4';
    var rightFill = '#dc528d';
    var leftRectangle = new Rectangle( 0, -20, width, 20, { fill: leftFill, lineWidth: 1, stroke: 'black' } );
    this.addChild( leftRectangle );
    var rightRectangle = new Rectangle( 0, -40, width, 20, { fill: rightFill, lineWidth: 1, stroke: 'black' } );
    this.addChild( rightRectangle );

    new DerivedProperty( [ leftFractionProperty ], function( leftFraction ) {
      return leftFraction * width;
    } ).linkAttribute( leftRectangle, 'rectWidth' );

    new DerivedProperty( [ rightFractionProperty ], function( rightFraction ) {
      return rightFraction * width;
    } ).linkAttribute( rightRectangle, 'rectWidth' );

    var linesNode = new Node( { pickable: false } );
    this.addChild( linesNode );

    //Create the fraction nodes, and size them to be about the same size as the 0/1 labels.  Cannot use maths to get the scaling exactly right since the font bounds are wonky, so just use a heuristic scale factor
    var fractionNodeScale = 0.22;
    var fractionTop = 14;
    var leftFractionNode = new FractionNode( leftFractionModel.numeratorProperty, leftFractionModel.denominatorProperty, {
      interactive: false,
      scale: fractionNodeScale,
      fill: leftFill,
      top: fractionTop
    } );
    this.addChild( leftFractionNode );
    var coloredTickStroke = 2;
    var leftFractionNodeTickMark = new Line( 0, 0, 0, 0, { lineWidth: coloredTickStroke, stroke: leftFill } );
    this.addChild( leftFractionNodeTickMark );

    var rightFractionNode = new FractionNode( rightFractionModel.numeratorProperty, rightFractionModel.denominatorProperty, {
      interactive: false,
      scale: fractionNodeScale,
      fill: rightFill,
      top: fractionTop
    } );
    this.addChild( rightFractionNode );
    var rightFractionNodeTickMark = new Line( 0, 0, 0, 0, { lineWidth: coloredTickStroke, stroke: rightFill } );
    this.addChild( rightFractionNodeTickMark );

    //When tick spacing or labeled ticks change, update the ticks
    //TODO: Could be redesigned so that the black ticks aren't changing when the numerators change, if it is a performance problem
    Property.multilink( [ visibleProperty,
        leftFractionModel.numeratorProperty,
        leftFractionModel.denominatorProperty,
        rightFractionModel.numeratorProperty,
        rightFractionModel.denominatorProperty ],
      function( visible, leftNumerator, leftDenominator, rightNumerator, rightDenominator ) {
        var lineHeight = 16;
        var leastCommonDenominator = NumberLineNode.leastCommonDenominator( leftDenominator, rightDenominator );
        var lines = [];
        var maxTickIndex = leastCommonDenominator;
        for ( var i = 0; i <= maxTickIndex; i++ ) {
          var distance = i / maxTickIndex * width;

          if ( visible || i === 0 || i === maxTickIndex ) {
            lines.push( new Line( distance, -lineHeight / 2, distance, lineHeight / 2, { lineWidth: 1.5, stroke: 'black' } ) );
          }
        }
        linesNode.children = lines;

        //Update the left/right fraction nodes for the fraction value and the colored tick mark
        var leftXOffset = (leftNumerator === 0 || leftNumerator === leftDenominator ) ? lineHeight :
                          Math.abs( leftNumerator / leftDenominator - rightNumerator / rightDenominator ) < 1E-6 ? lineHeight * 0.8 :
                          0;
        var leftCenterX = width * leftNumerator / leftDenominator - leftXOffset;
        leftFractionNode.centerX = leftCenterX;
        leftFractionNodeTickMark.setLine( leftCenterX, leftFractionNode.top, width * leftNumerator / leftDenominator, leftFractionNode.top - fractionTop );

        var rightXOffset = (rightNumerator === 0 || rightNumerator === rightDenominator) ? lineHeight :
                           Math.abs( rightNumerator / rightDenominator - leftNumerator / leftDenominator ) < 1E-6 ? lineHeight * 0.8 :
                           0;
        var rightCenterX = width * rightNumerator / rightDenominator + rightXOffset;
        rightFractionNode.centerX = rightCenterX;
        rightFractionNodeTickMark.setLine( rightCenterX, rightFractionNode.top, width * rightNumerator / rightDenominator, rightFractionNode.top - fractionTop );

        //Handle overlapping number labels, see https://github.com/phetsims/fraction-comparison/issues/31
        if ( leftFractionNode.bounds.intersectsBounds( rightFractionNode.bounds ) && Math.abs( rightNumerator / rightDenominator - leftNumerator / leftDenominator ) > 1E-6 ) {
          var overlapAmount = (leftFractionModel.fraction > rightFractionModel.fraction) ?
                              leftFractionNode.bounds.minX - rightFractionNode.bounds.maxX + 2 :
                              leftFractionNode.bounds.maxX - rightFractionNode.bounds.minX + 2;

          leftFractionNode.translate( -overlapAmount / 2 / fractionNodeScale, 0 );
          rightFractionNode.translate( +overlapAmount / 2 / fractionNodeScale, 0 );
        }
      } );

    var labelTop = linesNode.children[ 0 ].bounds.maxY;

    var zeroLabel = new Text( '0', { centerX: linesNode.children[ 0 ].centerX, top: labelTop, font: new PhetFont( { size: 26 } ) } );
    var oneLabel = new Text( '1', {
      centerX: linesNode.children[ linesNode.children.length - 1 ].centerX,
      top: labelTop,
      font: new PhetFont( { size: 26 } )
    } );

    this.addChild( zeroLabel );
    this.addChild( oneLabel );

    //Only show certain properties when the number line checkbox is selected
    visibleProperty.linkAttribute( leftRectangle, 'visible' );
    visibleProperty.linkAttribute( rightRectangle, 'visible' );
    visibleProperty.linkAttribute( leftFractionNode, 'visible' );
    visibleProperty.linkAttribute( rightFractionNode, 'visible' );
    visibleProperty.linkAttribute( leftFractionNodeTickMark, 'visible' );
    visibleProperty.linkAttribute( rightFractionNodeTickMark, 'visible' );

    this.mutate( options );
  }
  /**
   * @constructor
   * @param {Bounds2} layoutBounds - layout bounds of the screen view
   */
  function PlayAreaGridNode( layoutBounds, tandem ) {

    Node.call( this, { pickable: false } );
    var blueOptions = { fill: 'rgba(0,0,255,0.5)' };
    var greyOptions = { fill: 'rgba(200,200,200,0.5)' };
    var redOptions = { fill: 'rgba(250,0,50,0.45)' };

    var columns = PlayAreaMap.COLUMN_RANGES;
    var rows = PlayAreaMap.ROW_RANGES;
    var landmarks = PlayAreaMap.LANDMARK_RANGES;

    // draw each column
    var self = this;
    var i = 0;
    var range;
    var minValue;
    var maxValue;
    for ( range in columns ) {
      if ( columns.hasOwnProperty( range ) ) {
        if ( i % 2 === 0 ) {
          minValue = Math.max( layoutBounds.minX, columns[ range ].min );
          maxValue = Math.min( layoutBounds.maxX, columns[ range ].max );
          var width = maxValue - minValue;
          self.addChild( new Rectangle( minValue, 0, width, PlayAreaMap.HEIGHT, blueOptions ) );
        }
        i++;
      }
    }

    // draw each row
    for ( range in rows ) {
      if ( rows.hasOwnProperty( range ) ) {
        if ( i % 2 === 0 ) {
          minValue = Math.max( layoutBounds.minY, rows[ range ].min );
          maxValue = Math.min( layoutBounds.maxY, rows[ range ].max );
          var height = maxValue - minValue;
          self.addChild( new Rectangle( 0, minValue, PlayAreaMap.WIDTH, height, greyOptions ) );
        }
        i++;
      }
    }

    // draw rectangles around the landmark regions
    for ( range in landmarks ) {
      if ( landmarks.hasOwnProperty( range ) ) {
        minValue = Math.max( layoutBounds.minX, landmarks[ range ].min );
        maxValue = Math.min( layoutBounds.maxX, landmarks[ range ].max );
        var landmarkWidth = maxValue - minValue;
        self.addChild( new Rectangle( minValue, 0, landmarkWidth, PlayAreaMap.HEIGHT, redOptions ) );
      }
    }

    // draw the lines to along critical balloon locations along both x and y
    var lineOptions = { stroke: 'rgba(0, 0, 0,0.4)', lineWidth: 2, lineDash: [ 2, 4 ] };
    var xLocations = PlayAreaMap.X_LOCATIONS;
    var yLocations = PlayAreaMap.Y_LOCATIONS;
    var location;
    for ( location in xLocations ) {
      if ( xLocations.hasOwnProperty( location ) ) {
        self.addChild( new Line( xLocations[ location ], 0, xLocations[ location ], PlayAreaMap.HEIGHT, lineOptions ) );
      }
    }

    for ( location in yLocations ) {
      if ( yLocations.hasOwnProperty( location ) ) {
        self.addChild( new Line( 0, yLocations[ location ], PlayAreaMap.WIDTH, yLocations[ location ], lineOptions ) );
      }
    }
  }
  /**
   * @param {JohnTravoltageModel} model
   * @param {AppendageNode} armNode
   * @param {number} maxElectrons
   * @param {Tandem} tandem
   * @constructor
   */
  function ElectronLayerNode( model, armNode, maxElectrons, tandem ) {
    var self = this;

    Node.call( this );

    // Add larger delay time is used so that the assistive technology can finish speaking updates
    // from the aria-valuetext of the AppendageNode. Note that if the delay is too long, there is too much silence
    // between the change in charges and the alert.
    const electronUtterance = new Utterance( {
      alertStableDelay: 1000
    } );

    var priorCharge = 0;

    // a11y - when electrons enter or leave the body, announce this change with a status update to assistive technology
    var setElectronStatus = function() {
      var alertString;
      var currentCharge = model.electrons.length;

      if ( currentCharge >= priorCharge ) {
        alertString = StringUtils.fillIn( electronsTotalString, { value: currentCharge } );

      }
      else {
        var position = armNode.positionAtDischarge || '';

        var regionText = '';
        if ( armNode.regionAtDischarge && armNode.regionAtDischarge.text ) {
          regionText = armNode.regionAtDischarge.text.toLowerCase();
        }

        alertString = StringUtils.fillIn( electronsTotalAfterDischargeString, {
          oldValue: priorCharge,
          newValue: currentCharge,
          position: position,
          region: regionText
        } );
      }

      electronUtterance.alert = alertString;
      utteranceQueue.addToBack( electronUtterance );
      
      priorCharge = currentCharge;
    };

    // if new electron added to model - create and add new node to leg
    function electronAddedListener( added ) {

      // and the visual representation of the electron
      var newElectron = new ElectronNode( added, model.leg, model.arm, tandem.createTandem( added.tandem.tail ) );
      self.addChild( newElectron );

      // a11y - anounce the state of charges with a status update
      setElectronStatus();

      // If GC issues are noticeable from creating this IIFE, consider a map that maps model elements to 
      // corresponding view components, see https://github.com/phetsims/john-travoltage/issues/170
      var itemRemovedListener = function( removed ) {
        if ( removed === added ) {
          self.removeChild( newElectron );
          model.electrons.removeItemRemovedListener( itemRemovedListener );
        }
      };
      model.electrons.addItemRemovedListener( itemRemovedListener );
    }

    model.electrons.addItemAddedListener( electronAddedListener );
    model.electrons.forEach( electronAddedListener );

    // update status whenever an electron discharge has ended - disposal is not necessary
    model.dischargeEndedEmitter.addListener( setElectronStatus );

    // when the model is reset, update prior charge - disposal not necessary
    model.resetEmitter.addListener( function() {
      priorCharge = 0;
    } );
  }
Example #24
0
  /**
   * @param {NumberProperty} xProperty - x coordinate value
   * @param {NumberProperty} yProperty - y coordinate value
   * @param {BooleanProperty} valuesVisibleProperty - whether values are visible on the plot
   * @param {BooleanProperty} displacementVectorVisibleProperty - whether the horizontal displacement is displayed
   * @param {Object} [options]
   * @constructor
   * @abstract
   */
  function XYPointPlot( xProperty, yProperty, valuesVisibleProperty, displacementVectorVisibleProperty, options ) {

    options = _.extend( {

      // both axes
      axisFont: new PhetFont( 12 ),
      valueFont: new PhetFont( 12 ),

      // x axis
      minX: -1,
      maxX: 1,
      xString: 'x',
      xDecimalPlaces: 0,
      xUnits: '',
      xValueFill: 'black',
      xUnitLength: 1,
      xLabelMaxWidth: null,
      xValueBackgroundColor: null,

      // y axis
      minY: -1,
      maxY: 1,
      yString: 'y',
      yDecimalPlaces: 0,
      yUnits: '',
      yValueFill: 'black',
      yUnitLength: 1,
      yValueBackgroundColor: null,

      // point
      pointFill: 'black',
      pointRadius: 5,

      // phet-io
      tandem: Tandem.required

    }, options );

    // XY axes
    var axesNode = new XYAxes( {
      minX: options.minX,
      maxX: options.maxX,
      minY: options.minY,
      maxY: options.maxY,
      xString: options.xString,
      yString: options.yString,
      font: options.axisFont,
      xLabelMaxWidth: options.xLabelMaxWidth
    } );

    // point
    var pointNode = new Circle( options.pointRadius, {
      fill: options.pointFill
    } );

    // x nodes
    var xValueNode = new Text( '', {
      maxWidth: 150, // i18n
      fill: options.xValueFill,
      font: options.valueFont
    } );
    var xTickNode = new Line( 0, 0, 0, TICK_LENGTH, _.extend( TICK_OPTIONS, { centerY: 0 } ) );
    var xLeaderLine = new Line( 0, 0, 0, 1, LEADER_LINE_OPTIONS );
    var xVectorNode = new Line( 0, 0, 1, 0, { lineWidth: 3, stroke: HookesLawColors.DISPLACEMENT } );
    var xValueBackgroundNode = new Rectangle( 0, 0, 1, 1, { fill: options.xValueBackgroundColor } );

    // y nodes
    var yValueNode = new Text( '', {
      maxWidth: 150, // i18n
      fill: options.yValueFill,
      font: options.valueFont
    } );
    var yTickNode = new Line( 0, 0, TICK_LENGTH, 0, _.extend( TICK_OPTIONS, { centerX: 0 } ) );
    var yLeaderLine = new Line( 0, 0, 1, 0, LEADER_LINE_OPTIONS );
    var yValueBackgroundNode = new Rectangle( 0, 0, 1, 1, { fill: options.yValueBackgroundColor } );

    assert && assert( !options.children, 'XYPointPlot sets children' );
    options.children = [
      axesNode,
      xLeaderLine, xTickNode, xValueBackgroundNode, xValueNode, xVectorNode,
      yLeaderLine, yTickNode, yValueBackgroundNode, yValueNode,
      pointNode
    ];

    // visibility
    displacementVectorVisibleProperty.link( function( visible ) {
      var xFixed = Util.toFixedNumber( xProperty.get(), options.xDecimalPlaces ); // the displayed value
      xVectorNode.visible = ( visible && xFixed !== 0 );
    } );
    valuesVisibleProperty.link( function( visible ) {

      // x-axis nodes
      xValueNode.visible = visible;
      xValueBackgroundNode.visible = visible;
      xTickNode.visible = visible;
      xLeaderLine.visible = visible;

      // y axis nodes
      yValueNode.visible = visible;
      yValueBackgroundNode.visible = visible;
      yTickNode.visible = visible;
      yLeaderLine.visible = visible;
    } );

    xProperty.link( function( x ) {

      var xFixed = Util.toFixedNumber( x, options.xDecimalPlaces );
      var xView = options.xUnitLength * xFixed;

      // x vector
      xVectorNode.visible = ( xFixed !== 0 && displacementVectorVisibleProperty.get() ); // can't draw a zero-length arrow
      if ( xFixed !== 0 ) {
        xVectorNode.setLine( 0, 0, xView, 0 );
      }

      // x tick mark
      xTickNode.visible = ( xFixed !== 0 && valuesVisibleProperty.get() );
      xTickNode.centerX = xView;

      // x value
      var xText = Util.toFixed( xFixed, HookesLawConstants.DISPLACEMENT_DECIMAL_PLACES );
      xValueNode.text = StringUtils.format( pattern0Value1UnitsString, xText, options.xUnits );

      // placement of x value, so that it doesn't collide with y value or axes
      if ( options.minY === 0 ) {
        xValueNode.centerX = xView; // centered on the tick
        xValueNode.top = 12; // below the x axis
      }
      else {
        var X_SPACING = 6;
        if ( Math.abs( xView ) > ( X_SPACING + xValueNode.width / 2 ) ) {
          xValueNode.centerX = xView; // centered on the tick
        }
        else if ( xFixed >= 0 ) {
          xValueNode.left = X_SPACING; // to the right of the y axis
        }
        else {
          xValueNode.right = -X_SPACING; // to the left of the y axis
        }

        var Y_SPACING = 12;
        if ( yProperty.get() >= 0 ) {
          xValueNode.top = Y_SPACING; // below the x axis
        }
        else {
          xValueNode.bottom = -Y_SPACING; // above the x axis
        }
      }

      // x value background
      xValueBackgroundNode.setRect( 0, 0,
        xValueNode.width + ( 2 * VALUE_X_MARGIN ), xValueNode.height + ( 2 * VALUE_Y_MARGIN ),
        VALUE_BACKGROUND_CORNER_RADIUS, VALUE_BACKGROUND_CORNER_RADIUS );
      xValueBackgroundNode.center = xValueNode.center;
    } );

    yProperty.link( function( y ) {

      var yFixed = Util.toFixedNumber( y, options.yDecimalPlaces );
      var yView = yFixed * options.yUnitLength;

      // y tick mark
      yTickNode.visible = ( yFixed !== 0 && valuesVisibleProperty.get() );
      yTickNode.centerY = -yView;

      // y value
      var yText = Util.toFixed( yFixed, options.yDecimalPlaces );
      yValueNode.text = StringUtils.format( pattern0Value1UnitsString, yText, options.yUnits );

      // placement of y value, so that it doesn't collide with x value or axes
      var X_SPACING = 10;
      if ( xProperty.get() >= 0 ) {
        yValueNode.right = -X_SPACING; // to the left of the y axis
      }
      else {
        yValueNode.left = X_SPACING; // to the right of the y axis
      }

      var Y_SPACING = 4;
      if ( Math.abs( yView ) > Y_SPACING + yValueNode.height / 2 ) {
        yValueNode.centerY = -yView; // centered on the tick
      }
      else if ( yFixed >= 0 ) {
        yValueNode.bottom = -Y_SPACING; // above the x axis
      }
      else {
        yValueNode.top = Y_SPACING; // below the x axis
      }

      // y value background
      yValueBackgroundNode.setRect( 0, 0,
        yValueNode.width + ( 2 * VALUE_X_MARGIN ), yValueNode.height + ( 2 * VALUE_Y_MARGIN ),
        VALUE_BACKGROUND_CORNER_RADIUS, VALUE_BACKGROUND_CORNER_RADIUS );
      yValueBackgroundNode.center = yValueNode.center;
    } );

    // Move point and leader lines
    Property.multilink( [ xProperty, yProperty ],
      function( x, y ) {

        var xFixed = Util.toFixedNumber( x, options.xDecimalPlaces );
        var xView = options.xUnitLength * xFixed;
        var yView = -y * options.yUnitLength;

        // point
        pointNode.x = xView;
        pointNode.y = yView;

        // leader lines
        xLeaderLine.setLine( xView, 0, xView, yView );
        yLeaderLine.setLine( 0, yView, xView, yView );
      } );

    Node.call( this, options );
  }
Example #25
0
  function MassStackNode( model, mvt ) {
    var self = this;
    Node.call( this, {
      x: mvt.modelToViewX( model.poolDimensions.leftOpening.x1 )
    } );

    var totalHeight = 0; //height of all masses

    var placementRectWidth = mvt.modelToViewX( model.poolDimensions.leftOpening.x2 - model.poolDimensions.leftOpening.x1 );

    var placementRect = new Rectangle( 0, 0, placementRectWidth, 0 );
    var placementRectBorder = new Path( new Shape(),
      {
        stroke: '#000',
        lineWidth: 2,
        lineDash: [ 10, 5 ],
        fill: '#ffdcf0'
      } );

    this.addChild( placementRect );
    this.addChild( placementRectBorder );

    var controlMassStackPosition = function() {
      var dy = 0;
      model.stack.forEach( function( massModel ) {
        massModel.position = new Vector2( model.poolDimensions.leftOpening.x1 + massModel.width / 2, (model.poolDimensions.leftOpening.y2 - model.LEFT_WATER_HEIGHT + model.globalModel.leftDisplacement) - dy - massModel.height / 2 );
        dy += massModel.height;
      } );
    };

    var changeMassStack = function() {
      var totHeight = 0;
      model.stack.forEach( function( massModel ) {
        if ( massModel ) {
          totHeight += massModel.height;
        }
      } );
      totalHeight = totHeight;
      controlMassStackPosition();
    };

    model.globalModel.leftDisplacementProperty.link( function( displacement ) {
      self.bottom = mvt.modelToViewY( model.poolDimensions.leftOpening.y2 - model.LEFT_WATER_HEIGHT + displacement );
    } );

    model.masses.forEach( function( massModel ) {
      massModel.isDraggingProperty.link( function( isDragging ) {
        if ( isDragging ) {
          var placementrectHeight = mvt.modelToViewY( massModel.height );
          var placementrectY1 = -placementrectHeight - mvt.modelToViewY( totalHeight );
          var newBorder = new Shape().moveTo( 0, placementrectY1 )
            .lineTo( 0, placementrectY1 + placementrectHeight )
            .lineTo( placementRectWidth, placementrectY1 + placementrectHeight )
            .lineTo( placementRectWidth, placementrectY1 )
            .lineTo( 0, placementrectY1 );
          placementRectBorder.shape = newBorder;
          placementRectBorder.visible = true;
        }
        else {
          placementRectBorder.visible = false;
        }
      } );
    } );

    model.stack.addListeners( function() {
      changeMassStack();
    }, function() {
      changeMassStack();
    } );
  }
  /**
   * @param {KitCollection} collection
   * @param {boolean} isSingleCollectionMode
   * @param {Function} toModelBounds
   * @constructor
   */
  function CollectionAreaNode( collection, isSingleCollectionMode, toModelBounds ) {
    Node.call( this, {} );
    var self = this;

    this.collectionBoxNodes = [];

    var maximumBoxWidth = isSingleCollectionMode ? SingleCollectionBoxNode.maxWidth : MultipleCollectionBoxNode.maxWidth;
    var maximumBoxHeight = isSingleCollectionMode ? SingleCollectionBoxNode.maxHeight : MultipleCollectionBoxNode.maxHeight;

    var y = 0;

    // add nodes for all of our collection boxes.
    collection.collectionBoxes.forEach( function( collectionBox ) {
      var collectionBoxNode = isSingleCollectionMode ? new SingleCollectionBoxNode( collectionBox, toModelBounds ) : new MultipleCollectionBoxNode( collectionBox, toModelBounds );
      self.collectionBoxNodes.push( collectionBoxNode );

      // TODO: can we fix this up somehow to be better? easier way to force height?
      // center box horizontally and put at bottom vertically in our holder
      function layoutBoxNode() {
        // compute correct offsets
        var offsetX = ( maximumBoxWidth - collectionBoxNode.width ) / 2;
        var offsetY = maximumBoxHeight - collectionBoxNode.height;

        // only apply these if they are different. otherwise we run into infinite recursion
        if ( collectionBoxNode.x !== offsetX || collectionBoxNode.y !== offsetY ) {
          collectionBoxNode.setTranslation( offsetX, offsetY );
        }
      }

      layoutBoxNode();

      // also position if its size changes in the future
      collectionBoxNode.on( 'bounds', layoutBoxNode );

      var collectionBoxHolder = new Node();
      // enforce consistent bounds of the maximum size. reason: we don't want switching between collections to alter the positions of the collection boxes
      //REVIEW: Use Spacer
      collectionBoxHolder.addChild( new Rectangle( 0, 0, maximumBoxWidth, maximumBoxHeight, {
        visible: false,
        stroke: null
      } ) ); // TODO: Spacer node for Scenery?
      collectionBoxHolder.addChild( collectionBoxNode );
      self.addChild( collectionBoxHolder );
      collectionBoxHolder.top = y;
      y += collectionBoxHolder.height + 15; //REVIEW: VBox?
    } );

    /*---------------------------------------------------------------------------*
     * Reset Collection button
     *----------------------------------------------------------------------------*/
    var resetCollectionButton = new TextPushButton( resetCollectionString, {
      listener: function() {
        // when clicked, empty collection boxes
        collection.collectionBoxes.forEach( function( box ) {
          box.reset();
        } );
        collection.kits.forEach( function( kit ) {
          kit.reset();
        } );
      },
      font: new PhetFont( 14 ),
      baseColor: Color.ORANGE
    } );
    resetCollectionButton.touchArea = Shape.bounds( resetCollectionButton.bounds.dilated( 7 ) );

    function updateEnabled() {
      var enabled = false;
      collection.collectionBoxes.forEach( function( box ) {
        if ( box.quantityProperty.value > 0 ) {
          enabled = true;
        }
      } );
      resetCollectionButton.enabled = enabled;
    }

    // when any collection box quantity changes, re-update whether we are enabled
    collection.collectionBoxes.forEach( function( box ) {
      box.quantityProperty.link( updateEnabled );
    } );

    resetCollectionButton.top = y;
    this.addChild( resetCollectionButton );

    // center everything
    var centerX = this.width / 2; // TODO: better layout code
    this.children.forEach( function( child ) {
      child.centerX = centerX;
    } );
  }
Example #27
0
  /**
   * @param {MembraneChannel} membraneChannelModel
   * @param {ModelViewTransform2D} mvt
   * @constructor
   */
  function MembraneChannelNode( membraneChannelModel, mvt ) {
    var self = this;
    Node.call( this, {} );
    this.membraneChannelModel = membraneChannelModel;
    this.mvt = mvt;

    /**
     *  @private
     *  @param {Dimension2D} size
     *  @param {Color} color
     */
    function createEdgeNode( size, color ) {
      var shape = new Shape();
      var width = size.width;
      var height = size.height;

      shape.moveTo( -width / 2, height / 4 );
      shape.cubicCurveTo( -width / 2, height / 2, width / 2, height / 2, width / 2, height / 4 );
      shape.lineTo( width / 2, -height / 4 );
      shape.cubicCurveTo( width / 2, -height / 2, -width / 2, -height / 2, -width / 2, -height / 4 );
      shape.close();

      return new Path( shape, { fill: color, stroke: color.colorUtilsDarker( 0.3 ), lineWidth: 0.4 } );
    }

    var stringShape;
    var channelPath;

    // Create the channel representation.
    var channel = new Path( new Shape(), { fill: membraneChannelModel.getChannelColor(), lineWidth: 0 } );

    // Skip bounds computation to improve performance
    channel.computeShapeBounds = function() {return new Bounds2( 0, 0, 0, 0 );};

    // Create the edge representations.
    var edgeNodeWidth = (membraneChannelModel.overallSize.width - membraneChannelModel.channelSize.width) / 2;
    var edgeNodeHeight = membraneChannelModel.overallSize.height;
    var transformedEdgeNodeSize = new Dimension2( Math.abs( mvt.modelToViewDeltaX( edgeNodeWidth ) ), Math.abs( mvt.modelToViewDeltaY( edgeNodeHeight ) ) );
    var leftEdgeNode = createEdgeNode( transformedEdgeNodeSize, membraneChannelModel.getEdgeColor() );
    var rightEdgeNode = createEdgeNode( transformedEdgeNodeSize, membraneChannelModel.getEdgeColor() );

    // Create the layers for the channel the edges.  This makes offsets and rotations easier.  See addToCanvas on why
    // node layer is an instance member.
    this.channelLayer = new Node();
    this.addChild( this.channelLayer );
    this.channelLayer.addChild( channel );
    this.edgeLayer = new Node();
    this.addChild( this.edgeLayer );
    this.edgeLayer.addChild( leftEdgeNode );
    this.edgeLayer.addChild( rightEdgeNode );

    // gets created and updated only if channel has InactivationGate
    var inactivationGateBallNode;
    var inactivationGateString;
    var edgeColor = membraneChannelModel.getEdgeColor().colorUtilsDarker( 0.3 );

    if ( membraneChannelModel.getHasInactivationGate() ) {

      // Add the ball and string that make up the inactivation gate.
      inactivationGateString = new Path( new Shape(), { lineWidth: 0.5, stroke: Color.BLACK } );

      // Skip bounds computation to improve performance
      inactivationGateString.computeShapeBounds = function() {return new Bounds2( 0, 0, 0, 0 );};
      this.channelLayer.addChild( inactivationGateString );

      var ballDiameter = mvt.modelToViewDeltaX( membraneChannelModel.getChannelSize().width );

      // inactivationBallShape is always a circle, so use the optimized version.
      inactivationGateBallNode = new Circle( ballDiameter / 2, { fill: edgeColor, lineWidth: 0.5, stroke: edgeColor } );
      this.edgeLayer.addChild( inactivationGateBallNode );
    }

    //private
    function updateRepresentation() {

      // Set the channel width as a function of the openness of the membrane channel.
      var channelWidth = membraneChannelModel.getChannelSize().width * membraneChannelModel.getOpenness();
      var channelSize = new Dimension2( channelWidth, membraneChannelModel.getChannelSize().height );
      var transformedChannelSize = new Dimension2( Math.abs( mvt.modelToViewDeltaX( channelSize.width ) ), Math.abs( mvt.modelToViewDeltaY( channelSize.height ) ) );

      // Make the node a bit bigger than the channel so that the edges can be placed over it with no gaps.
      var oversizeFactor = 1.2; // was 1.1 in Java

      var width = transformedChannelSize.width * oversizeFactor;
      var height = transformedChannelSize.height * oversizeFactor;
      var edgeNodeBounds = leftEdgeNode.getBounds();
      var edgeWidth = edgeNodeBounds.width; // Assume both edges are the same size.

      channelPath = new Shape();
      channelPath.moveTo( 0, 0 );
      channelPath.quadraticCurveTo( (width + edgeWidth) / 2, height / 8, width + edgeWidth, 0 );
      channelPath.lineTo( width + edgeWidth, height );
      channelPath.quadraticCurveTo( (width + edgeWidth) / 2, height * 7 / 8, 0, height );
      channelPath.close();
      channel.setShape( channelPath );

      /*
       The Java Version uses computed bounds which is a bit expensive, the current x and y coordinates of the channel
       is manually calculated. This allows for providing a customized computedBounds function.
       Kept this code for reference. Ashraf
       var channelBounds = channel.getBounds();
       channel.x = -channelBounds.width / 2;
       channel.y = -channelBounds.height / 2;
       */

      channel.x = -(width + edgeWidth) / 2;
      channel.y = -height / 2;

      leftEdgeNode.x = -transformedChannelSize.width / 2 - edgeNodeBounds.width / 2;
      leftEdgeNode.y = 0;
      rightEdgeNode.x = transformedChannelSize.width / 2 + edgeNodeBounds.width / 2;
      rightEdgeNode.y = 0;

      // If this membrane channel has an inactivation gate, update it.
      if ( membraneChannelModel.getHasInactivationGate() ) {

        var transformedOverallSize =
          new Dimension2( mvt.modelToViewDeltaX( membraneChannelModel.getOverallSize().width ),
            mvt.modelToViewDeltaY( membraneChannelModel.getOverallSize().height ) );

        // Position the ball portion of the inactivation gate.
        var channelEdgeConnectionPoint = new Vector2( leftEdgeNode.centerX,
          leftEdgeNode.getBounds().getMaxY() );
        var channelCenterBottomPoint = new Vector2( 0, transformedChannelSize.height / 2 );
        var angle = -Math.PI / 2 * (1 - membraneChannelModel.getInactivationAmount());
        var radius = (1 - membraneChannelModel.getInactivationAmount()) * transformedOverallSize.width / 2 + membraneChannelModel.getInactivationAmount() * channelEdgeConnectionPoint.distance( channelCenterBottomPoint );

        var ballPosition = new Vector2( channelEdgeConnectionPoint.x + Math.cos( angle ) * radius,
          channelEdgeConnectionPoint.y - Math.sin( angle ) * radius );
        inactivationGateBallNode.x = ballPosition.x;
        inactivationGateBallNode.y = ballPosition.y;

        // Redraw the "string" (actually a strand of protein in real life)
        // that connects the ball to the gate.
        var ballConnectionPoint = new Vector2( inactivationGateBallNode.x, inactivationGateBallNode.y );

        var connectorLength = channelCenterBottomPoint.distance( ballConnectionPoint );
        stringShape = new Shape().moveTo( channelEdgeConnectionPoint.x, channelEdgeConnectionPoint.y )
          .cubicCurveTo( channelEdgeConnectionPoint.x + connectorLength * 0.25,
          channelEdgeConnectionPoint.y + connectorLength * 0.5, ballConnectionPoint.x - connectorLength * 0.75,
          ballConnectionPoint.y - connectorLength * 0.5, ballConnectionPoint.x, ballConnectionPoint.y );
        inactivationGateString.setShape( stringShape );
      }

    }

    function updateLocation() {
      self.channelLayer.translate( mvt.modelToViewPosition( membraneChannelModel.getCenterLocation() ) );
      self.edgeLayer.translate( mvt.modelToViewPosition( membraneChannelModel.getCenterLocation() ) );
    }

    function updateRotation() {
      // Rotate based on the model element's orientation (the Java Version rotates and then translates, here the
      // transformation order is reversed - Ashraf).
      self.channelLayer.setRotation( -membraneChannelModel.rotationalAngle + Math.PI / 2 );
      self.edgeLayer.setRotation( -membraneChannelModel.rotationalAngle + Math.PI / 2 );
    }

    // Update the representation and location.
    updateRepresentation();
    updateLocation();
    updateRotation();
  }
Example #28
0
  /**
   * @param {PushButtonModel} pushButtonModel
   * @param {Property} interactionStateProperty - A property that is used to drive the visual appearance of the button.
   * @param {Object} [options]
   * @constructor
   */
  function RoundButtonView( pushButtonModel, interactionStateProperty, options ) {
    this.buttonModel = pushButtonModel; // @protected // TODO: rename to pushButtonModel

    options = _.extend( {

      radius: ( options && options.content ) ? undefined : 30,
      content: null,
      cursor: 'pointer',
      baseColor: DEFAULT_COLOR,
      disabledBaseColor: ColorConstants.LIGHT_GRAY,
      minXMargin: 5, // Minimum margin in x direction, i.e. on left and right
      minYMargin: 5, // Minimum margin in y direction, i.e. on top and bottom
      fireOnDown: false,

      // pointer area dilation
      touchAreaDilation: 0, // radius dilation for touch area
      mouseAreaDilation: 0, // radius dilation for mouse area

      // pointer area shift
      touchAreaXShift: 0,
      touchAreaYShift: 0,
      mouseAreaXShift: 0,
      mouseAreaYShift: 0,

      stroke: undefined, // undefined by default, which will cause a stroke to be derived from the base color
      lineWidth: 0.5, // Only meaningful if stroke is non-null
      tandem: Tandem.optional, // This duplicates the parent option and works around https://github.com/phetsims/tandem/issues/50

      // By default, icons are centered in the button, but icons with odd
      // shapes that are not wrapped in a normalizing parent node may need to
      // specify offsets to line things up properly
      xContentOffset: 0,
      yContentOffset: 0,

      // Strategy for controlling the button's appearance, excluding any
      // content.  This can be a stock strategy from this file or custom.  To
      // create a custom one, model it off of the stock strategies defined in
      // this file.
      buttonAppearanceStrategy: RoundButtonView.ThreeDAppearanceStrategy,

      // Strategy for controlling the appearance of the button's content based
      // on the button's state.  This can be a stock strategy from this file,
      // or custom.  To create a custom one, model it off of the stock
      // version(s) defined in this file.
      contentAppearanceStrategy: RoundButtonView.FadeContentWhenDisabled,

      // a11y
      tagName: 'button',
      focusHighlightDilation: 5 // radius dilation for circular highlight
    }, options );

    Node.call( this );
    var content = options.content; // convenience variable
    var upCenter = new Vector2( options.xContentOffset, options.yContentOffset );

    // For performance reasons, the content should be unpickable.
    if ( content ) {
      content.pickable = false;
    }

    // Make the base color into a property so that the appearance strategy can update itself if changes occur.
    this.baseColorProperty = new PaintColorProperty( options.baseColor ); // @private

    // @private {PressListener}
    var pressListener = pushButtonModel.createListener( { tandem: options.tandem.createTandem( 'pressListener' ) } );
    this.addInputListener( pressListener );

    // Use the user-specified radius if present, otherwise calculate the
    // radius based on the content and the margin.
    var buttonRadius = options.radius ||
                       Math.max( content.width + options.minXMargin * 2, content.height + options.minYMargin * 2 ) / 2;

    // Create the basic button shape.
    var button = new Circle( buttonRadius, { fill: options.baseColor, lineWidth: options.lineWidth } );
    this.addChild( button );

    // Hook up the strategy that will control the basic button appearance.
    var buttonAppearanceStrategy = new options.buttonAppearanceStrategy(
      button,
      interactionStateProperty,
      this.baseColorProperty,
      options
    );

    // Add the content to the button.
    if ( content ) {
      content.center = upCenter;
      this.addChild( content );
    }

    // Hook up the strategy that will control the content appearance.
    var contentAppearanceStrategy = new options.contentAppearanceStrategy( content, interactionStateProperty );

    // Control the pointer state based on the interaction state.
    var self = this;

    function handleInteractionStateChanged( state ) {
      self.cursor = state === ButtonInteractionState.DISABLED ||
                    state === ButtonInteractionState.DISABLED_PRESSED ? null : 'pointer';
    }

    interactionStateProperty.link( handleInteractionStateChanged );

    // Dilate the pointer areas.
    this.touchArea = Shape.circle( options.touchAreaXShift, options.touchAreaYShift,
      buttonRadius + options.touchAreaDilation );
    this.mouseArea = Shape.circle( options.mouseAreaXShift, options.mouseAreaYShift,
      buttonRadius + options.mouseAreaDilation );

    // Set pickable such that sub-nodes are pruned from hit testing.
    this.pickable = null;

    // a11y
    this.focusHighlight = new Shape.circle( 0, 0, buttonRadius + options.focusHighlightDilation );

    // Mutate with the options after the layout is complete so that
    // width-dependent fields like centerX will work.
    this.mutate( options );

    // define a dispose function
    this.disposeRoundButtonView = function() {
      buttonAppearanceStrategy.dispose();
      contentAppearanceStrategy.dispose();
      pressListener.dispose();
      if ( interactionStateProperty.hasListener( handleInteractionStateChanged ) ) {
        interactionStateProperty.unlink( handleInteractionStateChanged );
      }
      this.baseColorProperty.dispose();
    };
  }
  /**
   * Constructor
   * @param {Shaker} shaker
   * @param {ModelViewTransform2} modelViewTransform
   * @constructor
   */
  function ShakerNode( shaker, modelViewTransform ) {

    var thisNode = this;
    Node.call( thisNode, { renderer: 'svg', rendererOptions: { cssTransform: true } } );

    // shaker image
    var imageNode = new Image( shakerImage );
    imageNode.setScaleMagnitude( 0.75 );

    // label
    var labelNode = new SubSupText( shaker.solute.formula, { font: new PhetFont( { size: 22, weight: 'bold' } ), fill: 'black' } );

    // arrows
    var downArrowShape = new Shape()
      .moveTo( 0, 0 )
      .lineTo( -ARROW_HEAD_WIDTH / 2, -ARROW_HEAD_LENGTH )
      .lineTo( -ARROW_TAIL_WIDTH / 2, -ARROW_HEAD_LENGTH )
      .lineTo( -ARROW_TAIL_WIDTH / 2, -ARROW_LENGTH )
      .lineTo( ARROW_TAIL_WIDTH / 2, -ARROW_LENGTH )
      .lineTo( ARROW_TAIL_WIDTH / 2, -ARROW_HEAD_LENGTH )
      .lineTo( ARROW_HEAD_WIDTH / 2, -ARROW_HEAD_LENGTH )
      .close();
    var downArrowNode = new Path( downArrowShape, {fill: ARROW_FILL, stroke: ARROW_STROKE } );
    downArrowNode.top = imageNode.bottom + 4;
    downArrowNode.centerX = imageNode.centerX;
    downArrowNode.pickable = false;
    downArrowNode.visible = false;

    var upArrowShape = new Shape()
      .moveTo( 0, 0 )
      .lineTo( -ARROW_HEAD_WIDTH / 2, ARROW_HEAD_LENGTH )
      .lineTo( -ARROW_TAIL_WIDTH / 2, ARROW_HEAD_LENGTH )
      .lineTo( -ARROW_TAIL_WIDTH / 2, ARROW_LENGTH )
      .lineTo( ARROW_TAIL_WIDTH / 2, ARROW_LENGTH )
      .lineTo( ARROW_TAIL_WIDTH / 2, ARROW_HEAD_LENGTH )
      .lineTo( ARROW_HEAD_WIDTH / 2, ARROW_HEAD_LENGTH )
      .close();
    var upArrowNode = new Path( upArrowShape, { fill: ARROW_FILL, stroke: ARROW_STROKE } );
    upArrowNode.bottom = imageNode.top - 4;
    upArrowNode.centerX = imageNode.centerX;
    upArrowNode.pickable = false;
    upArrowNode.visible = false;

    // common parent, to simplify rotation and label alignment.
    var parentNode = new Node();
    thisNode.addChild( parentNode );
    parentNode.addChild( imageNode );
    parentNode.addChild( labelNode );
    if ( SHOW_ARROWS ) {
      parentNode.addChild( upArrowNode );
      parentNode.addChild( downArrowNode );
    }
    parentNode.rotate( shaker.orientation - Math.PI ); // assumes that shaker points to the left in the image file

    // Manually adjust these values until the origin is in the middle hole of the shaker.
    parentNode.translate( -12, -imageNode.height / 2 );

    // origin
    if ( DEBUG_ORIGIN ) {
      thisNode.addChild( new Circle( { radius: 3, fill: 'red' } ) );
    }

    // sync location with model
    var shakerWasMoved = false;
    shaker.locationProperty.link( function( location ) {
      thisNode.translation = modelViewTransform.modelToViewPosition( location );
      shakerWasMoved = true;
      upArrowNode.visible = downArrowNode.visible = false;
    } );
    shakerWasMoved = false; // reset to false, because function is fired when link is performed

    // sync visibility with model
    shaker.visible.link( function( visible ) {
      thisNode.setVisible( visible );
    } );

    // sync solute with model
    shaker.solute.link( function( solute ) {
      // label the shaker with the solute formula
      labelNode.setText( solute.formula );
      // center the label on the shaker
      var capWidth = 0.3 * imageNode.width;
      labelNode.centerX = capWidth + ( imageNode.width - capWidth ) / 2;
      labelNode.centerY = imageNode.centerY;
    } );

    // interactivity
    thisNode.cursor = 'pointer';
    thisNode.addInputListener( new MovableDragHandler( shaker, modelViewTransform ) );
    thisNode.addInputListener( {
        enter: function() {
          upArrowNode.visible = downArrowNode.visible = !shakerWasMoved;
        },
        exit: function() {
          upArrowNode.visible = downArrowNode.visible = false;
        }
      }
    );
  }
  /**
   * @param {number} numberValue
   * @param {Function} addShapeToModel - A function for adding the created number  to the model
   * @param {Function} canPlaceShape - A function to determine if the PaperNumber can be placed on the board
   * @constructor
   */
  function PaperNumberCreatorNode( numberValue, addShapeToModel, combineNumbersIfApplicableCallback, canPlaceShape ) {

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

    // Create the node that the user will click upon to add a model element to the view.
    var representation = new Image( PaperImageCollection.getNumberImage( numberValue ) );
    representation.scale( 0.64, 0.55 );
    this.addChild( representation );

    // Add the listener that will allow the user to click on this and create a new shape, then position it in the model.
    this.addInputListener( new SimpleDragHandler( {

      parentScreen: null, // needed for coordinate transforms
      paperNumberModel: null,

      // Allow moving a finger (touch) across this node to interact with it
      allowTouchSnag: true,

      start: function( event, trail ) {

        // Find the parent screen by moving up the scene graph.
        var testNode = self;
        while ( testNode !== null ) {
          if ( testNode instanceof ScreenView ) {
            this.parentScreen = testNode;
            break;
          }
          testNode = testNode.parents[ 0 ]; // Move up the scene graph by one level
        }

        // Determine the initial position of the new element as a function of the event position and this node's bounds.
        var upperLeftCornerGlobal = self.parentToGlobalPoint( self.leftTop );
        var initialPositionOffset = upperLeftCornerGlobal.minus( event.pointer.point );
        var initialPosition = this.parentScreen.globalToLocalPoint( event.pointer.point.plus( initialPositionOffset ) );

        // Create and add the new model element.
        this.paperNumberModel = new PaperNumberModel( numberValue, initialPosition );
        this.paperNumberModel.userControlled = true;
        addShapeToModel( this.paperNumberModel );

      },

      translate: function( translationParams ) {
        this.paperNumberModel.setDestination( this.paperNumberModel.position.plus( translationParams.delta ) );
      },

      end: function( event, trail ) {
        this.paperNumberModel.userControlled = false;
        var droppedPoint = event.pointer.point;
        var droppedScreenPoint = this.parentScreen.globalToLocalPoint( event.pointer.point );
        //check if the user has dropped the number within the panel itself, if "yes" return to origin
        if ( !canPlaceShape( this.paperNumberModel, droppedScreenPoint ) ) {
          this.paperNumberModel.returnToOrigin( true );
          this.paperNumberModel = null;
          return;
        }
        combineNumbersIfApplicableCallback( this.paperNumberModel, droppedPoint );
        this.paperNumberModel = null;
      }
    } ) );

  }