getNextPosition3D: function( currentPosition3D, bounds, dt ) {

      // destination is assumed to always have a Z value of 0, i.e. at the "surface"
      var currentDestination3D = new Vector3(
        this.destinationProperty.get().x - this.offsetFromDestinationProperty.x,
        this.destinationProperty.get().y - this.offsetFromDestinationProperty.y,
        0
      );
      var currentDestination2D = new Vector2(
        this.destinationProperty.get().x - this.offsetFromDestinationProperty.x,
        this.destinationProperty.get().y - this.offsetFromDestinationProperty.y
      );
      var currentPosition2D = new Vector2( currentPosition3D.x, currentPosition3D.y );
      this.updateVelocityVector2D(
        currentPosition2D,
        new Vector2( currentDestination3D.x, currentDestination3D.y ),
        this.scalarVelocity2D
      );

      // make the Z velocity such that the front (i.e. z = 0) will be reached at the same time as the destination in XY
      // space
      var distanceToDestination2D = currentPosition2D.distance( this.destinationProperty.get() );
      var zVelocity;
      if ( distanceToDestination2D > 0 ) {
        zVelocity = Math.min(
          Math.abs( currentPosition3D.z ) / ( currentPosition2D.distance( this.destinationProperty.get() ) / this.scalarVelocity2D ),
          MAX_Z_VELOCITY
        );
      }
      else {
        zVelocity = MAX_Z_VELOCITY;
      }

      // make sure that the current motion won't move the model element past the destination
      var distanceToDestination = currentPosition2D.distance( currentDestination2D );
      if ( this.velocityVector2D.magnitude * dt > distanceToDestination ) {
        return currentDestination3D;
      }

      // calculate the next location based on the motion vector
      return new Vector3(
        currentPosition3D.x + this.velocityVector2D.x * dt,
        currentPosition3D.y + this.velocityVector2D.y * dt,
        Util.clamp( currentPosition3D.z + zVelocity * dt, -1, 0 )
      );
    }
Beispiel #2
0
  /**
   * @constructor
   */
  function AxonMembrane() {

    // @public - events emitted by instances of this type
    this.travelingActionPotentialStarted = new Emitter();
    this.travelingActionPotentialReachedCrossSection = new Emitter();
    this.lingeringCompleted = new Emitter();
    this.travelingActionPotentialEnded = new Emitter();

    // Traveling action potential that moves down the membrane.
    this.travelingActionPotential = null;

    //-----------------------------------------------------------------------------------------------------------------
    // Create the shape of the axon body
    //-----------------------------------------------------------------------------------------------------------------

    // @public - shape of the body of the axon
    this.axonBodyShape = new Shape();

    // points at which axon membrane would appear to vanish, used in shape creation
    var vanishingPoint = new Vector2(
      BODY_LENGTH * Math.cos( BODY_TILT_ANGLE ),
      BODY_LENGTH * Math.sin( BODY_TILT_ANGLE )
    );

    // Find the two points at which the shape will intersect the outer edge of the cross section.
    var r = NeuronConstants.DEFAULT_DIAMETER / 2 + this.getMembraneThickness() / 2;
    var theta = BODY_TILT_ANGLE + Math.PI * 0.45; // Multiplier tweaked a bit for improved appearance.
    var intersectionPointA = new Vector2( r * Math.cos( theta ), r * Math.sin( theta ) );
    theta += Math.PI;
    var intersectionPointB = new Vector2( r * Math.cos( theta ), r * Math.sin( theta ) );

    // Define the control points for the two curves.  Note that there is some tweaking in here, so change as needed
    // to get the desired look. If you can figure it out, that is.  Hints: The shape is drawn starting as a curve
    // from the vanishing point to intersection point A, then a line to intersection point B, then as a curve back to
    // the vanishing point.
    var angleToVanishingPt = Math.atan2(
      vanishingPoint.y - intersectionPointA.y,
      vanishingPoint.x - intersectionPointA.x );
    var controlPtRadius = intersectionPointA.distance( vanishingPoint ) * 0.33;
    var controlPtA1 = new Vector2(
      intersectionPointA.x + controlPtRadius * Math.cos( angleToVanishingPt + 0.15 ),
      intersectionPointA.y + controlPtRadius * Math.sin( angleToVanishingPt + 0.15 ) );
    controlPtRadius = intersectionPointA.distance( vanishingPoint ) * 0.67;
    var controlPtA2 = new Vector2(
      intersectionPointA.x + controlPtRadius * Math.cos( angleToVanishingPt - 0.5 ),
      intersectionPointA.y + controlPtRadius * Math.sin( angleToVanishingPt - 0.5 ) );

    var angleToIntersectionPt = Math.atan2( intersectionPointB.y - vanishingPoint.y,
      intersectionPointB.x - intersectionPointB.x );
    controlPtRadius = intersectionPointB.distance( vanishingPoint ) * 0.33;
    var controlPtB1 = new Vector2(
      vanishingPoint.x + controlPtRadius * Math.cos( angleToIntersectionPt + 0.1 ),
      vanishingPoint.y + controlPtRadius * Math.sin( angleToIntersectionPt + 0.1 ) );
    controlPtRadius = intersectionPointB.distance( vanishingPoint ) * 0.67;
    var controlPtB2 = new Vector2(
      vanishingPoint.x + controlPtRadius * Math.cos( angleToIntersectionPt - 0.25 ),
      vanishingPoint.y + controlPtRadius * Math.sin( angleToIntersectionPt - 0.25 ) );

    // @private - curves that define the boundaries of the body
    this.curveA = new Cubic(
      vanishingPoint,
      controlPtA2,
      controlPtA1,
      intersectionPointA
    );
    this.curveB = new Cubic(
      vanishingPoint,
      controlPtB1,
      controlPtB2,
      intersectionPointB
    );

    // In order to create the full shape, we reverse one of the curves and the connect the two curves together in
    // order to create the full shape of the axon body.
    this.axonBodyShape.moveTo( intersectionPointA.x, intersectionPointA.y );
    this.axonBodyShape.cubicCurveTo(
      controlPtA1.x,
      controlPtA1.y,
      controlPtA2.x,
      controlPtA2.y,
      vanishingPoint.x,
      vanishingPoint.y
    );
    this.axonBodyShape.cubicCurveTo(
      controlPtB1.x,
      controlPtB1.y,
      controlPtB2.x,
      controlPtB2.y,
      intersectionPointB.x,
      intersectionPointB.y
    );
    this.axonBodyShape.close();

    // @public - shape of the cross section of the membrane.	For now, and unless there is some reason to do otherwise,
    // the center of the cross section is positioned at the origin.
    this.crossSectionCircleCenter = Vector2.ZERO;
    this.crossSectionCircleRadius = NeuronConstants.DEFAULT_DIAMETER / 2;

    // @private - In order to avoid creating new Vector2 instances during animation, these instances are declared and
    // reused in the evaluateCurve method.
    this.ab = new Vector2( 0, 0 );
    this.bc = new Vector2( 0, 0 );
    this.cd = new Vector2( 0, 0 );
    this.abbc = new Vector2( 0, 0 );
    this.bbcd = new Vector2( 0, 0 );
  }
Beispiel #3
0
    //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 );
      }

    }
    /**
     * @param {Energy} incomingEnergy
     * @public
     * @override
     */
    preloadEnergyChunks( incomingEnergy ) {

      this.clearEnergyChunks();

      if ( incomingEnergy.amount === 0 || incomingEnergy.type !== EnergyType.LIGHT ) {

        // no energy chunk pre-loading needed
        return;
      }

      const absorptionBounds = this.getAbsorptionShape().bounds;
      const lowerLeftOfPanel = new Vector2( absorptionBounds.minX, absorptionBounds.minY );
      const upperRightOfPanel = new Vector2( absorptionBounds.maxX, absorptionBounds.maxY );
      const crossLineAngle = upperRightOfPanel.minus( lowerLeftOfPanel ).angle;
      const crossLineLength = lowerLeftOfPanel.distance( upperRightOfPanel );
      const dt = 1 / EFACConstants.FRAMES_PER_SECOND;
      let energySinceLastChunk = EFACConstants.ENERGY_PER_CHUNK * 0.99;
      let preloadComplete = false;

      // simulate energy chunks moving through the system
      while ( !preloadComplete ) {

        // full energy rate generates too many chunks, so an adjustment factor is used
        energySinceLastChunk += incomingEnergy.amount * dt * 0.4;

        // determine if time to add a new chunk
        if ( energySinceLastChunk >= EFACConstants.ENERGY_PER_CHUNK ) {
          let initialPosition;
          if ( this.energyChunkList.length === 0 ) {

            // for predictability of the algorithm, add the first chunk to the center of the panel
            initialPosition = lowerLeftOfPanel.plus(
              new Vector2( crossLineLength * 0.5, 0 ).rotated( crossLineAngle )
            );
          }
          else {

            // choose a random location along the center portion of the cross line
            initialPosition = lowerLeftOfPanel.plus(
              new Vector2( crossLineLength * ( 0.5 * phet.joist.random.nextDouble() + 0.25 ), 0 ).rotated( crossLineAngle )
            );
          }

          const newEnergyChunk = new EnergyChunk(
            EnergyType.ELECTRICAL,
            initialPosition,
            Vector2.ZERO,
            this.energyChunksVisibleProperty
          );

          this.energyChunkList.push( newEnergyChunk );

          // add a "mover" that will move this energy chunk to the bottom of the solar panel
          this.electricalEnergyChunkMovers.push( new EnergyChunkPathMover(
            newEnergyChunk,
            EnergyChunkPathMover.createPathFromOffsets( this.positionProperty.get(), [ CONVERGENCE_POINT_OFFSET ] ),
            this.chooseChunkSpeedOnPanel( newEnergyChunk ) )
          );

          // update energy since last chunk
          energySinceLastChunk -= EFACConstants.ENERGY_PER_CHUNK;
        }

        this.moveElectricalEnergyChunks( dt );

        if ( this.outgoingEnergyChunks.length > 0 ) {

          // an energy chunk has made it all the way through the system, which completes the pre-load
          preloadComplete = true;
        }
      }
    }