Ejemplo n.º 1
0
  QUnit.test( 'Sceneless node handling', function( assert ) {
    var a = new Path( null );
    var b = new Path( null );
    var c = new Path( null );

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

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

    a.validateBounds();

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

    b.validateBounds();

    assert.ok( true, 'so we have at least 1 test in this set' );
  } );
 var createHoseIcon = function() {
   var icon = new Path( new Shape().moveTo( 0, 0 ).arc( -16, 8, 8, -Math.PI / 2, Math.PI / 2, true ).lineTo( 10, 16 ).lineTo( 10, 0 ).lineTo( 0, 0 ), {stroke: 'grey', lineWidth: 1, fill: '#00FF00'} );
   icon.addChild( new Image( nozzleImg, { cursor: 'pointer', rotation: Math.PI / 2, scale: 0.8, left: icon.right, bottom: icon.bottom + 3} ) );
   return icon;
 };
Ejemplo n.º 3
0
  /**
   * @param {MovableShape} movableShape
   * @param {Bounds2} dragBounds
   * @constructor
   */
  function ShapeNode( movableShape, dragBounds ) {
    Node.call( this, { cursor: 'pointer' } );
    var self = this;
    this.color = movableShape.color; // @public

    // Set up the mouse and touch areas for this node so that this can still be grabbed when invisible.
    this.touchArea = movableShape.shape;
    this.mouseArea = movableShape.shape;

    // Set up a root node whose visibility and opacity will be manipulated below.
    var rootNode = new Node();
    this.addChild( rootNode );

    // Create the shadow
    var shadow = new Path( movableShape.shape, {
      fill: SHADOW_COLOR,
      leftTop: SHADOW_OFFSET
    } );
    rootNode.addChild( shadow );

    // Create the primary representation
    var representation = new Path( movableShape.shape, {
      fill: movableShape.color,
      stroke: Color.toColor( movableShape.color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ),
      lineWidth: 1,
      lineJoin: 'round'
    } );
    rootNode.addChild( representation );

    // Add the grid
    representation.addChild( new Grid( representation.bounds.dilated( -BORDER_LINE_WIDTH ), UNIT_LENGTH, {
      lineDash: [ 0, 3, 1, 0 ],
      stroke: 'black'
    } ) );

    // Move this node as the model representation moves
    movableShape.positionProperty.link( function( position ) {
      self.leftTop = position;
    } );

    // Because a composite shape is often used to depict the overall shape when a shape is on the placement board, this
    // element may become invisible unless it is user controlled, animating, or fading.
    var visibleProperty = new DerivedProperty( [
        movableShape.userControlledProperty,
        movableShape.animatingProperty,
        movableShape.fadeProportionProperty,
        movableShape.invisibleWhenStillProperty ],
      function( userControlled, animating, fadeProportion, invisibleWhenStill ) {
        return ( userControlled || animating || fadeProportion > 0 || !invisibleWhenStill );
      } );

    // Opacity is also a derived property, range is 0 to 1.
    var opacityProperty = new DerivedProperty( [
        movableShape.userControlledProperty,
        movableShape.animatingProperty,
        movableShape.fadeProportionProperty ],
      function( userControlled, animating, fadeProportion ) {
        if ( userControlled || animating ) {
          // The shape is either being dragged by the user or is moving to a location, so should be fully opaque.
          return 1;
        }
        else if ( fadeProportion > 0 ) {
          // The shape is fading away.
          return 1 - fadeProportion;
        }
        else {
          // The shape is not controlled by the user, animated, or fading, so it is most likely placed on the board.
          // If it is visible, it will be translucent, since some of the games use shapes in this state to place over
          // other shapes for comparative purposes.
          return OPACITY_OF_TRANSLUCENT_SHAPES;
        }
      }
    );

    opacityProperty.link( function( opacity ) {
      rootNode.opacity = opacity;
    } );

    visibleProperty.link( function( visible ) {
      rootNode.visible = visible;
    } );

    var shadowVisibilityProperty = new DerivedProperty(
      [ movableShape.userControlledProperty, movableShape.animatingProperty ],
      function( userControlled, animating ) {
        return ( userControlled || animating );
      } );

    shadowVisibilityProperty.linkAttribute( shadow, 'visible' );

    function updatePickability(){
      // To avoid certain complications, this node should not be pickable if it is animating or fading.
      self.pickable = !movableShape.animatingProperty.get() && movableShape.fadeProportionProperty.get() === 0;
    }

    movableShape.animatingProperty.link( function() {
      updatePickability();
    } );

    movableShape.fadeProportionProperty.link( function( fadeProportion ) {
      updatePickability();
    } );

    // Adjust the drag bounds to compensate for the shape that that the entire shape will stay in bounds.
    var shapeDragBounds = new Bounds2(
      dragBounds.minX,
      dragBounds.minY,
      dragBounds.maxX - movableShape.shape.bounds.width,
      dragBounds.maxY - movableShape.shape.bounds.height
    );

    // Add the listener that will allow the user to drag the shape around.
    this.addInputListener( new MovableDragHandler( movableShape.positionProperty, {

      dragBounds: shapeDragBounds,

      // Allow moving a finger (touch) across a node to pick it up.
      allowTouchSnag: true,

      startDrag: function( event, trail ) {
        movableShape.userControlledProperty.set( true );
      },

      endDrag: function( event, trail ) {
        movableShape.userControlledProperty.set( false );
      }
    } ) );
  }
  /**
   * @constructor
   *
   * @param {boolean} isPositiveDown
   * @param {Object} [options]
   */
  function BatteryGraphicNode( isPositiveDown, options ) {
    Node.call( this );

    // @private {boolean}
    this.isPositiveDown = isPositiveDown;

    var middleY = isPositiveDown ? ( BATTERY_MAIN_HEIGHT - BATTERY_POSITIVE_SIDE_HEIGHT ) : BATTERY_POSITIVE_SIDE_HEIGHT;
    var terminalTopY = isPositiveDown ? 0 : -BATTERY_POSITIVE_TERMINAL_HEIGHT;
    var terminalRadius = isPositiveDown ? BATTERY_NEGATIVE_TERMINAL_RADIUS : BATTERY_POSITIVE_TERMINAL_RADIUS;

    var bottomSideShape = new Shape().ellipticalArc( 0, middleY, BATTERY_MAIN_RADIUS, BATTERY_SECONDARY_RADIUS, 0, 0, Math.PI, false )
                                     .ellipticalArc( 0, BATTERY_MAIN_HEIGHT, BATTERY_MAIN_RADIUS, BATTERY_SECONDARY_RADIUS, 0, Math.PI, 0, true )
                                     .close();
    var topSideShape = new Shape().ellipticalArc( 0, middleY, BATTERY_MAIN_RADIUS, BATTERY_SECONDARY_RADIUS, 0, 0, Math.PI, false )
                                  .ellipticalArc( 0, 0, BATTERY_MAIN_RADIUS, BATTERY_SECONDARY_RADIUS, 0, Math.PI, 0, true )
                                  .close();
    var topShape = new Shape().ellipticalArc( 0, 0, BATTERY_MAIN_RADIUS, BATTERY_SECONDARY_RADIUS, 0, 0, Math.PI * 2, false ).close();
    var terminalTopShape = new Shape().ellipticalArc( 0, terminalTopY, terminalRadius, terminalRadius * BATTERY_PERSPECTIVE_RATIO, 0, 0, Math.PI * 2, false ).close();
    var terminalSideShape = new Shape().ellipticalArc( 0, terminalTopY, terminalRadius, terminalRadius * BATTERY_PERSPECTIVE_RATIO, 0, 0, Math.PI, false )
                                     .ellipticalArc( 0, 0, terminalRadius, terminalRadius * BATTERY_PERSPECTIVE_RATIO, 0, Math.PI, 0, true )
                                     .close();

    // @public {Shape}
    this.terminalShape = terminalTopShape;

    var bottomSide = new Path( bottomSideShape, {
      fill: isPositiveDown ? POSITIVE_GRADIENT : NEGATIVE_GRADIENT,
      stroke: STROKE_COLOR,
      lineWidth: LINE_WIDTH
    } );
    var topSide = new Path( topSideShape, {
      fill: isPositiveDown ? NEGATIVE_GRADIENT : POSITIVE_GRADIENT,
      stroke: STROKE_COLOR,
      lineWidth: LINE_WIDTH
    } );
    var top = new Path( topShape, {
      fill: isPositiveDown ? NEGATIVE_COLOR : POSITIVE_COLOR,
      stroke: STROKE_COLOR,
      lineWidth: LINE_WIDTH
    } );
    var terminal = new Path( terminalTopShape, {
      fill: TERMINAL_COLOR,
      stroke: STROKE_COLOR,
      lineWidth: LINE_WIDTH
    } );

    if ( !isPositiveDown ) {
      this.terminalShape = this.terminalShape.shapeUnion( terminalSideShape );
      terminal.addChild( new Path( terminalSideShape, {
        fill: TERMINAL_GRADIENT,
        stroke: STROKE_COLOR,
        lineWidth: LINE_WIDTH
      } ) );
    }

    this.children = [
      top, topSide, bottomSide, terminal
    ];

    this.mutate( options );
  }
  /**
   * @param {MultipleParticleModel} multipleParticleModel - model of the simulation
   * @param {ModelViewTransform2} modelViewTransform
   * @param {boolean} volumeControlEnabled - set true to enable volume control by pushing the lid using a finger from above
   * @param {boolean} pressureGaugeEnabled - set true to show the pressure gauge
   * @constructor
   */
  function ParticleContainerNode( multipleParticleModel, modelViewTransform, volumeControlEnabled, pressureGaugeEnabled ) {

    Node.call( this, { preventFit: true } );
    var self = this;

    // @private, view bounds for the particle area, everything is basically constructed and positioned based on this
    this.particleAreaViewBounds = new Bounds2(
      modelViewTransform.modelToViewX( 0 ),
      modelViewTransform.modelToViewY( 0 ) + modelViewTransform.modelToViewDeltaY( multipleParticleModel.getInitialParticleContainerHeight() ),
      modelViewTransform.modelToViewX( 0 ) + modelViewTransform.modelToViewDeltaX( multipleParticleModel.getParticleContainerWidth() ),
      modelViewTransform.modelToViewY( 0 )
    );

    // @private
    this.multipleParticleModel = multipleParticleModel;
    this.modelViewTransform = modelViewTransform;
    this.previousContainerViewSize = this.particleAreaViewBounds.height;

    // add nodes for the various layers
    var preParticleLayer = new Node();
    this.addChild( preParticleLayer );
    this.particlesCanvasNode = new ParticleImageCanvasNode( multipleParticleModel.particles, modelViewTransform, {
      canvasBounds: SOMConstants.SCREEN_VIEW_OPTIONS.layoutBounds.dilated( 500, 500 ) // dilation amount empirically determined
    } );
    this.addChild( this.particlesCanvasNode );
    var postParticleLayer = new Node();
    this.addChild( postParticleLayer );

    // set up variables used to create and position the various parts of the container
    var containerWidthWithMargin = modelViewTransform.modelToViewDeltaX( multipleParticleModel.getParticleContainerWidth() ) +
                                   2 * CONTAINER_X_MARGIN;
    var topEllipseRadiusX = containerWidthWithMargin / 2;
    var topEllipseRadiusY = topEllipseRadiusX * PERSPECTIVE_TILT_FACTOR;

    // shape of the ellipse at the top of the container
    var topEllipseShape = new Shape().ellipticalArc(
      topEllipseRadiusX,
      0,
      topEllipseRadiusX,
      topEllipseRadiusY,
      0,
      0,
      2 * Math.PI,
      false
    );

    // add the elliptical opening at the top of the container, must be behind particles in z-order
    preParticleLayer.addChild( new Path( topEllipseShape, {
      lineWidth: 1,
      stroke: '#444444',
      centerX: this.particleAreaViewBounds.centerX,
      centerY: this.particleAreaViewBounds.minY
    } ) );

    // create and add the node that will act as the elliptical background for the lid, other nodes may be added later
    var containerLid = new Path( topEllipseShape, {
      fill: 'rgba( 126, 126, 126, 0.8 )',
      centerX: this.particleAreaViewBounds.centerX
    } );
    postParticleLayer.addChild( containerLid );

    if ( volumeControlEnabled ) {

      // Add the pointing hand, the finger of which can push down on the top of the container.
      var pointingHandNode = new PointingHandNode( multipleParticleModel, modelViewTransform, {
        centerX: this.particleAreaViewBounds.centerX + 30 // offset empirically determined
      } );
      postParticleLayer.addChild( pointingHandNode );

      // Add the handle to the lid.
      var handleAreaEllipseShape = topEllipseShape.transformed( Matrix3.scale( 0.8 ) ); // scale empirically determined
      var handleAreaEllipse = new Path( handleAreaEllipseShape, {
        lineWidth: 1,
        stroke: '#888888',
        fill: 'rgba( 200, 200, 200, 0.5 )',
        centerX: containerLid.width / 2,
        centerY: 0
      } );
      containerLid.addChild( handleAreaEllipse );
      var handleNode = new HandleNode( { scale: 0.28, attachmentFill: 'black', gripLineWidth: 4 } );
      handleNode.centerX = containerLid.width / 2;
      handleNode.bottom = handleAreaEllipse.centerY + 5; // position tweaked a bit to look better
      containerLid.addChild( handleNode );
    }

    if ( pressureGaugeEnabled ) {

      // Add the pressure meter.
      var pressureMeter = new DialGaugeNode( multipleParticleModel );
      pressureMeter.right = this.particleAreaViewBounds.minX + this.particleAreaViewBounds.width * 0.2;
      postParticleLayer.addChild( pressureMeter );
    }

    // define a function to evaluate the bottom edge of the ellipse at the top, used for relative positioning
    function getEllipseLowerEdgeYPos( distanceFromLeftEdge ) {
      var x = distanceFromLeftEdge - topEllipseRadiusX;
      return topEllipseRadiusY * Math.sqrt( 1 - Math.pow( x, 2 ) / ( Math.pow( topEllipseRadiusX, 2 ) ) );
    }

    // define a bunch of variable that will be used in the process of drawing the main container
    var outerShapeTiltFactor = topEllipseRadiusY * 1.28; // empirically determined multiplier that makes curve match lid
    var cutoutShapeTiltFactor = outerShapeTiltFactor * 0.55; // empirically determined multiplier that looks good
    var cutoutHeight = this.particleAreaViewBounds.getHeight() - 2 * CONTAINER_CUTOUT_Y_MARGIN;
    var cutoutTopY = getEllipseLowerEdgeYPos( CONTAINER_CUTOUT_X_MARGIN ) + CONTAINER_CUTOUT_Y_MARGIN;
    var cutoutBottomY = cutoutTopY + cutoutHeight;
    var cutoutWidth = containerWidthWithMargin - 2 * CONTAINER_CUTOUT_X_MARGIN;

    // create and add the main container node, excluding the bevel
    var mainContainer = new Path( new Shape()
      .moveTo( 0, 0 )

      // top curve, y-component of control points made to match up with lower edge of the lid
      .cubicCurveTo(
        0,
        outerShapeTiltFactor,
        containerWidthWithMargin,
        outerShapeTiltFactor,
        containerWidthWithMargin,
        0
      )

      // line from outer top right to outer bottom right
      .lineTo( containerWidthWithMargin, this.particleAreaViewBounds.height )

      // bottom outer curve
      .cubicCurveTo(
        containerWidthWithMargin,
        this.particleAreaViewBounds.height + outerShapeTiltFactor,
        0,
        this.particleAreaViewBounds.height + outerShapeTiltFactor,
        0,
        this.particleAreaViewBounds.height
      )

      // left outer side
      .lineTo( 0, 0 )

      // start drawing the cutout, must be drawn in opposite direction from outer shape to make the hole appear
      .moveTo( CONTAINER_CUTOUT_X_MARGIN, cutoutTopY )

      // left inner line
      .lineTo( CONTAINER_CUTOUT_X_MARGIN, cutoutBottomY )

      // bottom inner curve
      .quadraticCurveTo(
        containerWidthWithMargin / 2,
        cutoutBottomY + cutoutShapeTiltFactor,
        containerWidthWithMargin - CONTAINER_CUTOUT_X_MARGIN,
        cutoutBottomY
      )

      // line from inner bottom right to inner top right
      .lineTo( containerWidthWithMargin - CONTAINER_CUTOUT_X_MARGIN, cutoutTopY )

      // top inner curve
      .quadraticCurveTo(
        containerWidthWithMargin / 2,
        cutoutTopY + cutoutShapeTiltFactor,
        CONTAINER_CUTOUT_X_MARGIN,
        cutoutTopY
      )

      .close(),
      {
        fill: new LinearGradient( 0, 0, containerWidthWithMargin, 0 )
          .addColorStop( 0, '#6D6D6D' )
          .addColorStop( 0.1, '#8B8B8B' )
          .addColorStop( 0.2, '#AEAFAF' )
          .addColorStop( 0.4, '#BABABA' )
          .addColorStop( 0.7, '#A3A4A4' )
          .addColorStop( 0.75, '#8E8E8E' )
          .addColorStop( 0.8, '#737373' )
          .addColorStop( 0.9, '#646565' ),
        opacity: 0.9,
        centerX: this.particleAreaViewBounds.centerX,
        top: this.particleAreaViewBounds.minY
      }
    );
    postParticleLayer.addChild( mainContainer );

    var bevel = new Node( { opacity: 0.9 } );

    var leftBevelEdge = new Path(
      new Shape()
        .moveTo( 0, 0 )
        .lineTo( 0, cutoutHeight )
        .lineTo( BEVEL_WIDTH, cutoutHeight - BEVEL_WIDTH )
        .lineTo( BEVEL_WIDTH, BEVEL_WIDTH )
        .lineTo( 0, 0 )
        .close(),
      {
        fill: new LinearGradient( 0, 0, 0, cutoutHeight )
          .addColorStop( 0, '#525252' )
          .addColorStop( 0.3, '#515151' )
          .addColorStop( 0.4, '#4E4E4E' )
          .addColorStop( 0.5, '#424242' )
          .addColorStop( 0.6, '#353535' )
          .addColorStop( 0.7, '#2a2a2a' )
          .addColorStop( 0.8, '#292929' )
      }
    );
    bevel.addChild( leftBevelEdge );

    var rightBevelEdge = new Path(
      new Shape()
        .moveTo( 0, BEVEL_WIDTH )
        .lineTo( 0, cutoutHeight - BEVEL_WIDTH )
        .lineTo( BEVEL_WIDTH, cutoutHeight )
        .lineTo( BEVEL_WIDTH, 0 )
        .lineTo( 0, BEVEL_WIDTH )
        .close(),
      {
        left: cutoutWidth - BEVEL_WIDTH,
        fill: new LinearGradient( 0, 0, 0, cutoutHeight )
          .addColorStop( 0, '#8A8A8A' )
          .addColorStop( 0.2, '#747474' )
          .addColorStop( 0.3, '#525252' )
          .addColorStop( 0.6, '#8A8A8A' )
          .addColorStop( 0.9, '#A2A2A2' )
          .addColorStop( 0.95, '#616161' )
      }
    );
    bevel.addChild( rightBevelEdge );

    var topBevelEdge = new Path(
      new Shape()
        .moveTo( 0, 0 )
        .quadraticCurveTo( cutoutWidth / 2, cutoutShapeTiltFactor, cutoutWidth, 0 )
        .lineTo( cutoutWidth - BEVEL_WIDTH, BEVEL_WIDTH )
        .quadraticCurveTo( cutoutWidth / 2, cutoutShapeTiltFactor + BEVEL_WIDTH, BEVEL_WIDTH, BEVEL_WIDTH )
        .lineTo( 0, 0 )
        .close(),
      {
        lineWidth: 0,
        stroke: 'white',
        fill: new LinearGradient( 0, 0, cutoutWidth, 0 )
          .addColorStop( 0, '#2E2E2E' )
          .addColorStop( 0.2, '#323232' )
          .addColorStop( 0.3, '#363636' )
          .addColorStop( 0.4, '#3E3E3E' )
          .addColorStop( 0.5, '#4B4B4B' )
          .addColorStop( 0.9, '#525252' )
      }
    );
    bevel.addChild( topBevelEdge );

    var bottomBevelEdge = new Path(
      new Shape()
        .moveTo( BEVEL_WIDTH, 0 )
        .quadraticCurveTo( cutoutWidth / 2, cutoutShapeTiltFactor, cutoutWidth - BEVEL_WIDTH, 0 )
        .lineTo( cutoutWidth, BEVEL_WIDTH )
        .quadraticCurveTo( cutoutWidth / 2, cutoutShapeTiltFactor + BEVEL_WIDTH, 0, BEVEL_WIDTH )
        .lineTo( BEVEL_WIDTH, 0 )
        .close(),
      {
        top: cutoutHeight - BEVEL_WIDTH,
        fill: new LinearGradient( 0, 0, cutoutWidth, 0 )
          .addColorStop( 0, '#5D5D5D' )
          .addColorStop( 0.2, '#717171' )
          .addColorStop( 0.3, '#7C7C7C' )
          .addColorStop( 0.4, '#8D8D8D' )
          .addColorStop( 0.5, '#9E9E9E' )
          .addColorStop( 0.5, '#A2A2A2' )
          .addColorStop( 0.9, '#A3A3A3' )
      }
    );
    bevel.addChild( bottomBevelEdge );

    // Position and add the bevel.
    bevel.centerX = this.particleAreaViewBounds.centerX;
    bevel.top = this.particleAreaViewBounds.minY + cutoutTopY;
    postParticleLayer.addChild( bevel );

    // Define a function for updating the position and appearance of the pressure gauge.
    function updatePressureGaugePosition() {

      if ( !pressureMeter ) {
        // nothing to update, so bail out
        return;
      }

      var containerHeight = self.multipleParticleModel.particleContainerHeightProperty.get();

      if ( !self.multipleParticleModel.isExplodedProperty.get() ) {
        if ( pressureMeter.getRotation() !== 0 ) {
          pressureMeter.setRotation( 0 );
        }
        pressureMeter.top = self.particleAreaViewBounds.top - 75; // empirical position adjustment to connect to lid
        pressureMeter.setElbowHeight(
          PRESSURE_METER_ELBOW_OFFSET + Math.abs( self.modelViewTransform.modelToViewDeltaY(
            MultipleParticleModel.PARTICLE_CONTAINER_INITIAL_HEIGHT - containerHeight
          ) )
        );
      }
      else {

        // The container is exploding, so move the gauge up and spin it.
        var deltaHeight = self.modelViewTransform.modelToViewDeltaY( containerHeight ) - self.previousContainerViewSize;
        pressureMeter.rotate( deltaHeight * 0.01 * Math.PI );
        pressureMeter.centerY = pressureMeter.centerY + deltaHeight * 2;
      }
    }

    // Monitor the height of the container in the model and adjust the view when changes occur.
    multipleParticleModel.particleContainerHeightProperty.link( function( containerHeight, oldContainerHeight ) {

      if ( oldContainerHeight ) {
        self.previousContainerViewSize = modelViewTransform.modelToViewDeltaY( oldContainerHeight );
      }

      var lidYPosition = modelViewTransform.modelToViewY( containerHeight );

      containerLid.centerY = lidYPosition;

      if ( multipleParticleModel.isExplodedProperty.get() ) {

        // the container has exploded, so rotate the lid as it goes up so that it looks like it has been blown off.
        var deltaY = oldContainerHeight - containerHeight;
        var rotationAmount = deltaY * Math.PI * 0.00008; // multiplier empirically determined
        containerLid.rotateAround( containerLid.center, rotationAmount );
      }

      // update the position of the pointing hand
      pointingHandNode && pointingHandNode.setFingertipYPosition( lidYPosition );

      // update the pressure gauge position (if present)
      updatePressureGaugePosition();
    } );

    // Monitor the model for changes in the exploded state of the container and update the view as needed.
    multipleParticleModel.isExplodedProperty.link( function( isExploded, wasExploded ) {

      if ( !isExploded && wasExploded ) {

        // return the lid to the top of the container
        containerLid.setRotation( 0 );
        containerLid.centerX = modelViewTransform.modelToViewX( multipleParticleModel.getParticleContainerWidth() / 2 );
        containerLid.centerY = modelViewTransform.modelToViewY(
          multipleParticleModel.particleContainerHeightProperty.get()
        );

        // return the pressure gauge to its original position
        updatePressureGaugePosition();
      }
    } );
  }