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 ) ); }
/** * @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 ); } }; }
/** * @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 ) ); } ); }
/** * 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(); }
/** * * @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 ) ); }
//------------------------------------------------------------------------------------- /** * @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 } ); }
/** * 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; }
/** * @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(); } }; }
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 ); } ); };
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; } ); }
/** * @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 ); }
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; } ); }
/** * @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(); }
/** * @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; } } ) ); }