/** * @param {BaseGameModel} model * @param {HTMLImageElement[][]} levelImages - grid of images for the level-selection buttons, ordered by level * @param {function[]} rewardFactoryFunctions - functions that create nodes for the game reward, ordered by level * @constructor */ function BaseGameScreenView( model, levelImages, rewardFactoryFunctions ) { ScreenView.call( this, GLConstants.SCREEN_VIEW_OPTIONS ); // sounds var audioPlayer = new GameAudioPlayer( model.soundEnabledProperty ); // @private one parent node for each 'phase' of the game this.settingsNode = new SettingsNode( model, this.layoutBounds, levelImages ); this.playNode = new PlayNode( model, this.layoutBounds, this.visibleBoundsProperty, audioPlayer ); this.resultsNode = new ResultsNode( model, this.layoutBounds, audioPlayer, rewardFactoryFunctions ); // rendering order this.addChild( this.resultsNode ); this.addChild( this.playNode ); this.addChild( this.settingsNode ); // game 'phase' changes // unlink unnecessary because BaseGameScreenView exists for the lifetime of the sim. var self = this; model.gamePhaseProperty.link( function( gamePhase ) { self.settingsNode.visible = ( gamePhase === GamePhase.SETTINGS ); self.playNode.visible = ( gamePhase === GamePhase.PLAY ); self.resultsNode.visible = ( gamePhase === GamePhase.RESULTS ); } ); }
/** * @constructor */ function DialogsScreenView() { ScreenView.call( this ); // dialog will be created the first time the button is pressed, lazily because Dialog // requires sim bounds during Dialog construction var dialog = null; var modalDialogButton = new RectangularPushButton( { content: new Text( 'modal dialog', { font: BUTTON_FONT } ), listener: function() { if ( !dialog ) { dialog = createDialog( true ); } dialog.show(); }, left: this.layoutBounds.left + 100, top: this.layoutBounds.top + 100 } ); this.addChild( modalDialogButton ); // var nonModalDialogButton = new RectangularPushButton( { // content: new Text( 'non-modal dialog', { font: BUTTON_FONT } ), // listener: function() { // createDialog( false ).show(); // }, // left: modalDialogButton.right + 20, // top: modalDialogButton.top // } ); // this.addChild( nonModalDialogButton ); }
/** * @param {gameModel} model - Faraday's Law simulation model object * @constructor */ function FaradaysLawView( model ) { ScreenView.call( this, { renderer: 'svg', layoutBounds: FaradaysLawConstants.LAYOUT_BOUNDS } ); // coils var bottomCoilNode = new CoilNode( CoilTypeEnum.FOUR_COIL, { x: model.bottomCoil.position.x, y: model.bottomCoil.position.y } ); var topCoilNode = new CoilNode( CoilTypeEnum.TWO_COIL, { x: model.topCoil.position.x, y: model.topCoil.position.y } ); // aligner this.aligner = new Aligner( model, bottomCoilNode.endRelativePositions, topCoilNode.endRelativePositions ); // voltmeter and bulb created var voltMeterNode = new VoltMeterNode( model.voltMeterModel.thetaProperty, {} ); var bulbNode = new BulbNode( model.voltMeterModel.thetaProperty, { centerX: this.aligner.bulbPosition.x, centerY: this.aligner.bulbPosition.y } ); // wires this.addChild( new CoilsWiresNode( this.aligner, model.showSecondCoilProperty ) ); this.addChild( new VoltMeterWiresNode( this.aligner, voltMeterNode ) ); // bulb added this.addChild( bulbNode ); // coils added this.addChild( bottomCoilNode ); this.addChild( topCoilNode ); model.showSecondCoilProperty.linkAttribute( topCoilNode, 'visible' ); // control panel this.addChild( new ControlPanelNode( model ) ); // voltmeter added voltMeterNode.center = this.aligner.voltmeterPosition; this.addChild( voltMeterNode ); // magnet this.addChild( new MagnetNodeWithField( model ) ); // move coils to front bottomCoilNode.frontImage.detach(); this.addChild( bottomCoilNode.frontImage ); bottomCoilNode.frontImage.center = model.bottomCoil.position.plus( new Vector2( CoilNode.xOffset, 0 ) ); topCoilNode.frontImage.detach(); this.addChild( topCoilNode.frontImage ); topCoilNode.frontImage.center = model.topCoil.position.plus( new Vector2( CoilNode.xOffset + CoilNode.twoOffset, 0 ) ); model.showSecondCoilProperty.linkAttribute( topCoilNode.frontImage, 'visible' ); }
function AcidBaseSolutionsView( model ) { ScreenView.call( this, { renderer: 'svg' } ); // add control panel this.addChild( new ControlPanel( model ).mutate( {right: this.layoutBounds.maxX} ) ); // add workspace this.addChild( new Workspace( model ) ); }
/** * @param {BarMagnetModel} model * @constructor */ function ExampleScreenView( model ) { var thisView = this; ScreenView.call( thisView ); // model-view transform var modelViewTransform = ModelViewTransform2.createOffsetScaleMapping( new Vector2( thisView.layoutBounds.width / 2, thisView.layoutBounds.height / 2 ), 1 ); thisView.addChild( new BarMagnetNode( model.barMagnet, modelViewTransform ) ); thisView.addChild( new ControlPanel( model, { x: 50, y: 50 } ) ); }
/** * @param {ProjectileMotionModel} projectileMotionModel * @constructor */ function ProjectileMotionScreenView( projectileMotionModel ) { ScreenView.call( this ); // Reset All button var resetAllButton = new ResetAllButton( { listener: function() { projectileMotionModel.reset(); }, right: this.layoutBounds.maxX - 10, bottom: this.layoutBounds.maxY - 10 } ); this.addChild( resetAllButton ); }
/** * @param {BeersLawModel} model * @param {ModelViewTransform2} modelViewTransform * @param {Tandem} tandem * @constructor */ function BeersLawScreenView( model, modelViewTransform, tandem ) { ScreenView.call( this, _.extend( { tandem: tandem }, BLLConstants.SCREEN_VIEW_OPTIONS ) ); var lightNode = new LightNode( model.light, modelViewTransform, tandem.createTandem( 'lightNode' ) ); var cuvetteNode = new CuvetteNode( model.cuvette, model.solutionProperty, modelViewTransform, BLLQueryParameters.cuvetteSnapInterval, tandem.createTandem( 'cuvetteNode' ) ); var beamNode = new BeamNode( model.beam ); var detectorNode = new ATDetectorNode( model.detector, model.light, modelViewTransform, tandem.createTandem( 'detectorNode' ) ); var wavelengthControls = new WavelengthControls( model.solutionProperty, model.light, tandem.createTandem( 'wavelengthControls' ) ); var rulerNode = new BLLRulerNode( model.ruler, modelViewTransform, tandem.createTandem( 'rulerNode' ) ); var comboBoxListParent = new Node( { maxWidth: 500 } ); var solutionControls = new SolutionControls( model.solutions, model.solutionProperty, comboBoxListParent, tandem.createTandem( 'solutionControls' ), { maxWidth: 575 } ); // Reset All button var resetAllButton = new ResetAllButton( { scale: 1.32, listener: function() { model.reset(); wavelengthControls.reset(); }, tandem: tandem.createTandem( 'resetAllButton' ) } ); // Rendering order this.addChild( wavelengthControls ); this.addChild( resetAllButton ); this.addChild( solutionControls ); this.addChild( detectorNode ); this.addChild( cuvetteNode ); this.addChild( beamNode ); this.addChild( lightNode ); this.addChild( rulerNode ); this.addChild( comboBoxListParent ); // last, so that combo box list is on top // Layout for things that don't have a location in the model. { // below the light wavelengthControls.left = lightNode.left; wavelengthControls.top = lightNode.bottom + 20; // below cuvette solutionControls.left = cuvetteNode.left; solutionControls.top = cuvetteNode.bottom + 60; // bottom right resetAllButton.right = this.layoutBounds.right - 30; resetAllButton.bottom = this.layoutBounds.bottom - 30; } }
/** * Constructor for the ExampleScreenView, it creates the bar magnet node and control panel node. * * @param {ExampleModel} model - the model for the entire screen * @constructor */ function ExampleScreenView( model ) { ScreenView.call( this, { layoutBounds: new Bounds2( 0, 0, 768, 504 ) } ); // model-view transform var center = new Vector2( this.layoutBounds.width / 2, this.layoutBounds.height / 2 ); var modelViewTransform = ModelViewTransform2.createOffsetScaleMapping( center, 1 ); this.addChild( new BarMagnetNode( model.barMagnet, modelViewTransform ) ); this.addChild( new ControlPanel( model, { x: 50, y: 50 } ) ); }
/** * @param {MakingTensExploreModel} makingTensCommonModel * @constructor */ function MakingTensCommonView( makingTensModel, screenBounds, paperNumberNodeLayer ) { var self = this; ScreenView.call( this, { layoutBounds: screenBounds } ); self.makingTensModel = makingTensModel; self.paperNumberLayerNode = new Node(); paperNumberNodeLayer.addChild( self.paperNumberLayerNode ); self.addUserCreatedNumberModel = makingTensModel.addUserCreatedNumberModel.bind( makingTensModel ); self.combineNumbersIfApplicableCallback = this.combineNumbersIfApplicable.bind( this ); function handlePaperNumberAdded( addedNumberModel ) { // Add a representation of the number. var paperNumberNode = new PaperNumberNode( addedNumberModel, self.addUserCreatedNumberModel, self.combineNumbersIfApplicableCallback ); self.paperNumberLayerNode.addChild( paperNumberNode ); // Move the shape to the front of this layer when grabbed by the user. addedNumberModel.userControlledProperty.link( function( userControlled ) { if ( userControlled ) { paperNumberNode.moveToFront(); } } ); makingTensModel.residentNumberModels.addItemRemovedListener( function removalListener( removedNumberModel ) { if ( removedNumberModel === addedNumberModel ) { self.paperNumberLayerNode.removeChild( paperNumberNode ); makingTensModel.residentNumberModels.removeItemRemovedListener( removalListener ); } } ); } //Initial Number Node creation makingTensModel.residentNumberModels.forEach( handlePaperNumberAdded ); // Observe new items makingTensModel.residentNumberModels.addItemAddedListener( handlePaperNumberAdded ); }
function ShapeshiftScreenView( model ) { phet.joist.display.backgroundColor = '#000'; var self = this; window.screenView = this; var shapeshiftScreenView = this; this.model = model; var bounds = new Bounds2( 0, 0, 1024, 618 ); ScreenView.call( this, { layoutBounds: bounds } ); var showHomeScreen = function() { self.showNode( self.homeScreen ); }; this.arcadeNode = new ArcadeGameNode( model, this.layoutBounds, this.visibleBoundsProperty, showHomeScreen ); this.adventureNode = new AdventureGameNode( model, this.layoutBounds, this.visibleBoundsProperty, showHomeScreen ); this.freePlayNode = new FreeplayGameNode( model, this.layoutBounds, this.visibleBoundsProperty, showHomeScreen ); this.preventFit = true; this.homeScreen = new HomeScreen( bounds, function() { self.showNode( self.arcadeNode ); }, function() { self.showNode( self.adventureNode ); }, function() { self.showNode( self.freePlayNode ); } ); self.showNode( self.homeScreen ); var level = phet.chipper.getQueryParameter( 'level' ); if ( level ) { this.showNode( this.adventureNode ); } }
/** * Constructor for the MotionView * @param {MotionModel} model model for the entire screen * @constructor */ function MotionView( model ) { //Constants and fields this.model = model; //Call super constructor ScreenView.call( this, {renderer: 'svg'} ); //Variables for this constructor, for convenience var motionView = this; var width = this.layoutBounds.width; var height = this.layoutBounds.height; //Constants var skyHeight = 362; var groundHeight = height - skyHeight; //Create the static background var skyGradient = new LinearGradient( 0, 0, 0, skyHeight ).addColorStop( 0, '#02ace4' ).addColorStop( 1, '#cfecfc' ); this.sky = new Rectangle( -width, -skyHeight, width * 3, skyHeight * 2, {fill: skyGradient, pickable: false} ); this.groundNode = new Rectangle( -width, skyHeight, width * 3, groundHeight * 2, {fill: '#c59a5b', pickable: false} ); this.addChild( this.sky ); this.addChild( this.groundNode ); //Create the dynamic (moving) background this.addChild( new MovingBackgroundNode( model, this.layoutBounds.width / 2 ).mutate( { layerSplit: true } ) ); //Add toolbox backgrounds for the objects var boxHeight = 180; this.addChild( new Rectangle( 10, height - boxHeight - 10, 300, boxHeight, 10, 10, {fill: '#e7e8e9', stroke: '#000000', lineWidth: 1, pickable: false} ) ); this.addChild( new Rectangle( width - 10 - 300, height - boxHeight - 10, 300, boxHeight, 10, 10, { fill: '#e7e8e9', stroke: '#000000', lineWidth: 1, pickable: false} ) ); //Add the pusher this.addChild( new PusherNode( model, this.layoutBounds.width ) ); //Add the skateboard if on the 'motion' screen if ( model.skateboard ) { this.addChild( new Image( skateboardImage, {centerX: width / 2, y: 315 + 12, pickable: false} ) ); } //Create the slider var disableText = function( node ) { return function( length ) {node.fill = length === 0 ? 'gray' : 'black';}; }; var disableLeftProperty = new DerivedProperty( [model.fallenProperty, model.fallenDirectionProperty], function( fallen, fallenDirection ) { return fallen && fallenDirection === 'left'; } ); var disableRightProperty = new DerivedProperty( [model.fallenProperty, model.fallenDirectionProperty], function( fallen, fallenDirection ) { return fallen && fallenDirection === 'right'; } ); var sliderLabel = new Text( Strings.appliedForce, {font: new PhetFont( 22 ), centerX: width / 2, y: 430} ); var slider = new HSlider( -500, 500, 300, model.appliedForceProperty, model.speedClassificationProperty, disableLeftProperty, disableRightProperty, {zeroOnRelease: true, centerX: width / 2 + 1, y: 535} ).addNormalTicks(); this.addChild( sliderLabel ); this.addChild( slider ); //Position the units to the right of the text box. var readout = new Text( '???', {font: new PhetFont( 22 ), pickable: false} ); readout.bottom = slider.top - 15; model.appliedForceProperty.link( function( appliedForce ) { readout.text = appliedForce.toFixed( 0 ) + ' ' + Strings.newtons; //TODO: i18n message format readout.centerX = width / 2; } ); //Make 'Newtons Readout' stand out but not look like a text entry field this.textPanelNode = new Rectangle( 0, 0, readout.right - readout.left + 50, readout.height + 4, {fill: 'white', stroke: 'lightGray', centerX: width / 2, top: readout.y - readout.height + 2, pickable: false} ); this.addChild( this.textPanelNode ); this.addChild( readout ); //Show left arrow button 'tweaker' to change the applied force in increments of 50 var leftArrowButton = new ArrowButton( 'left', function() { model.appliedForce = Math.max( model.appliedForce - 50, -500 ); }, {rectangleYMargin: 7, rectangleXMargin: 10, right: this.textPanelNode.left - 6, centerY: this.textPanelNode.centerY} ); //Do not allow the user to apply a force that would take the object beyond its maximum velocity model.multilink( ['appliedForce', 'speedClassification', 'stackSize'], function( appliedForce, speedClassification, stackSize ) {leftArrowButton.setEnabled( stackSize > 0 && (speedClassification === 'LEFT_SPEED_EXCEEDED' ? false : appliedForce > -500 ) );} ); this.addChild( leftArrowButton ); //Show right arrow button 'tweaker' to change the applied force in increments of 50 var rightArrowButton = new ArrowButton( 'right', function() { model.appliedForce = Math.min( model.appliedForce + 50, 500 ); }, {rectangleYMargin: 7, rectangleXMargin: 10, left: this.textPanelNode.right + 6, centerY: this.textPanelNode.centerY} ); //Do not allow the user to apply a force that would take the object beyond its maximum velocity model.multilink( ['appliedForce', 'speedClassification', 'stackSize'], function( appliedForce, speedClassification, stackSize ) { rightArrowButton.setEnabled( stackSize > 0 && (speedClassification === 'RIGHT_SPEED_EXCEEDED' ? false : appliedForce < 500 ) ); } ); this.addChild( rightArrowButton ); model.stack.lengthProperty.link( disableText( sliderLabel ) ); model.stack.lengthProperty.link( disableText( readout ) ); model.stack.lengthProperty.link( function( length ) { slider.enabled = length > 0; } ); //Create the speedometer. Specify the location after construction so we can set the 'top' var speedometerNode = new SpeedometerNode( model.velocityProperty, Strings.speed, MotionConstants.MAX_SPEED ).mutate( {x: width / 2, top: 2} ); model.showSpeedProperty.linkAttribute( speedometerNode, 'visible' ); //Move away from the stack if the stack getting too high. No need to record this in the model since it will always be caused deterministically by the model. //Use Tween.JS to smoothly animate var itemsCentered = new Property( true ); model.stack.lengthProperty.link( function() { //Move both the accelerometer and speedometer if the stack is getting too high, based on the height of items in the stack var stackHeightThreshold = 160; if ( motionView.stackHeight > stackHeightThreshold && itemsCentered.value ) { itemsCentered.value = false; new TWEEN.Tween( speedometerNode ).to( { centerX: 300}, 400 ).easing( TWEEN.Easing.Cubic.InOut ).start(); if ( accelerometerNode ) { new TWEEN.Tween( accelerometerWithTickLabels ).to( { centerX: 300}, 400 ).easing( TWEEN.Easing.Cubic.InOut ).start(); } } else if ( motionView.stackHeight <= stackHeightThreshold && !itemsCentered.value ) { itemsCentered.value = true; new TWEEN.Tween( speedometerNode ).to( { x: width / 2}, 400 ).easing( TWEEN.Easing.Cubic.InOut ).start(); if ( accelerometerNode ) { new TWEEN.Tween( accelerometerWithTickLabels ).to( { centerX: width / 2}, 400 ).easing( TWEEN.Easing.Cubic.InOut ).start(); } } } ); this.addChild( speedometerNode ); //Create and add the control panel var controlPanel = new MotionControlPanel( model ); this.addChild( controlPanel ); //Reset all button goes beneath the control panel var resetButton = new ResetAllButton( model.reset.bind( model ), {scale: 88 / 103} ).mutate( {centerX: controlPanel.centerX, top: controlPanel.bottom + 5} ); this.addChild( resetButton ); //Add the accelerometer, if on the final screen if ( model.accelerometer ) { var accelerometerNode = new AccelerometerNode( model.accelerationProperty ); var labelAndAccelerometer = new VBox( {pickable: false, children: [new Text( 'Acceleration', {font: new PhetFont( 18 )} ), accelerometerNode]} ); var tickLabel = function( label, tick ) { return new Text( label, {pickable: false, font: new PhetFont( 16 ), centerX: tick.centerX, top: tick.bottom + 27} ); }; var accelerometerWithTickLabels = new Node( {children: [labelAndAccelerometer, tickLabel( '-20', accelerometerNode.ticks[0] ), tickLabel( '0', accelerometerNode.ticks[2] ), tickLabel( '20', accelerometerNode.ticks[4] )], centerX: width / 2, y: 135, pickable: false} ); model.showAccelerationProperty.linkAttribute( accelerometerWithTickLabels, 'visible' ); this.addChild( accelerometerWithTickLabels ); } //Iterate over the items in the model and create and add nodes for each one this.itemNodes = []; for ( var i = 0; i < model.items.length; i++ ) { var item = model.items[i]; var Constructor = item.bucket ? WaterBucketNode : ItemNode; var itemNode = new Constructor( model, motionView, item, item.image, item.sittingImage || item.image, item.holdingImage || item.image, model.showMassesProperty ); this.itemNodes.push( itemNode ); //Provide a reference from the item model to its view so that view dimensions can be looked up easily item.view = itemNode; this.addChild( itemNode ); } //Add the force arrows & associated readouts in front of the items var arrowScale = 0.3; this.sumArrow = new ReadoutArrow( Strings.sumOfForces, '#96c83c', this.layoutBounds.width / 2, 230, model.sumOfForcesProperty, model.showValuesProperty, {labelPosition: 'top', arrowScale: arrowScale} ); model.multilink( ['showForce', 'showSumOfForces'], function( showForce, showSumOfForces ) {motionView.sumArrow.visible = showForce && showSumOfForces;} ); this.sumOfForcesText = new Text( Strings.sumOfForcesEqualsZero, {pickable: false, font: new PhetFont( { size: 16, weight: 'bold' } ), centerX: width / 2, y: 200} ); model.multilink( ['showForce', 'showSumOfForces', 'sumOfForces'], function( showForce, showSumOfForces, sumOfForces ) {motionView.sumOfForcesText.visible = showForce && showSumOfForces && !sumOfForces;} ); this.appliedForceArrow = new ReadoutArrow( Strings.appliedForce, '#e66e23', this.layoutBounds.width / 2, 280, model.appliedForceProperty, model.showValuesProperty, {labelPosition: 'side', arrowScale: arrowScale} ); this.frictionArrow = new ReadoutArrow( Strings.friction, '#e66e23', this.layoutBounds.width / 2, 280, model.frictionForceProperty, model.showValuesProperty, {labelPosition: 'side', arrowScale: arrowScale} ); this.addChild( this.sumArrow ); this.addChild( this.appliedForceArrow ); this.addChild( this.frictionArrow ); this.addChild( this.sumOfForcesText ); //On the motion screens, when the 'Friction' label overlaps the force vector it should be displaced vertically model.multilink( ['appliedForce', 'frictionForce'], function( appliedForce, frictionForce ) { var sameDirection = (appliedForce < 0 && frictionForce < 0) || (appliedForce > 0 && frictionForce > 0); motionView.frictionArrow.labelPosition = sameDirection ? 'bottom' : 'side'; } ); model.showForceProperty.linkAttribute( this.appliedForceArrow, 'visible' ); model.showForceProperty.linkAttribute( this.frictionArrow, 'visible' ); //After the view is constructed, move one of the blocks to the top of the stack. model.viewInitialized( this ); }
/** * @param {MolarityModel} model * @constructor */ function MolarityView( model ) { var thisView = this; ScreenView.call( thisView, { layoutBounds: new Bounds2( 0, 0, 1100, 700 ) } ); var valuesVisibleProperty = new Property( false ); // beaker, with solution and precipitate inside of it var beakerNode = new BeakerNode( model.solution, MolarityModel.SOLUTION_VOLUME_RANGE.max, valuesVisibleProperty ); var cylinderSize = beakerNode.getCylinderSize(); var solutionNode = new SolutionNode( cylinderSize, beakerNode.getCylinderEndHeight(), model.solution, MolarityModel.SOLUTION_VOLUME_RANGE.max ); var precipitateNode = new PrecipitateNode( model.solution, cylinderSize, beakerNode.getCylinderEndHeight(), model.maxPrecipitateAmount ); var saturatedIndicator = new SaturatedIndicator( model.solution ); // solute control var foregroundNode = new Node(); var soluteComboBox = new SoluteComboBox( model.solutes, model.solution.soluteProperty, foregroundNode ); // slider for controlling amount of solute var soluteAmountSlider = new VerticalSlider( soluteAmountString, StringUtils.format( pattern_parentheses_0text, molesString ), noneString, lotsString, new Dimension2( 5, cylinderSize.height ), model.solution.soluteAmountProperty, MolarityModel.SOLUTE_AMOUNT_RANGE, SOLUTE_AMOUNT_DECIMAL_PLACES, units_molesString, valuesVisibleProperty ); // slider for controlling volume of solution, sized to match tick marks on the beaker var volumeSliderHeight = ( MolarityModel.SOLUTION_VOLUME_RANGE.getLength() / MolarityModel.SOLUTION_VOLUME_RANGE.max ) * cylinderSize.height; var solutionVolumeSlider = new VerticalSlider( solutionVolumeString, StringUtils.format( pattern_parentheses_0text, litersString ), lowString, fullString, new Dimension2( 5, volumeSliderHeight ), model.solution.volumeProperty, MolarityModel.SOLUTION_VOLUME_RANGE, VOLUME_DECIMAL_PLACES, units_litersString, valuesVisibleProperty ); // concentration display var concentrationBarSize = new Dimension2( 40, cylinderSize.height + 50 ); var concentrationDisplay = new ConcentrationDisplay( model.solution, MolarityModel.CONCENTRATION_DISPLAY_RANGE, valuesVisibleProperty, concentrationBarSize ); // Show Values check box var showValuesCheckBox = CheckBox.createTextCheckBox( showValuesString, { font: new PhetFont( 22 ) }, valuesVisibleProperty ); showValuesCheckBox.touchArea = Shape.rectangle( showValuesCheckBox.left, showValuesCheckBox.top - 15, showValuesCheckBox.width, showValuesCheckBox.height + 30 ); // Reset All button var resetAllButton = new ResetAllButton( { listener: function() { valuesVisibleProperty.reset(); model.reset(); }, scale: 1.32 } ); // rendering order this.addChild( solutionNode ); this.addChild( beakerNode ); this.addChild( precipitateNode ); this.addChild( saturatedIndicator ); this.addChild( soluteAmountSlider ); this.addChild( solutionVolumeSlider ); this.addChild( concentrationDisplay ); this.addChild( showValuesCheckBox ); this.addChild( resetAllButton ); this.addChild( soluteComboBox ); this.addChild( foregroundNode ); // layout for things that don't have a location in the model { soluteAmountSlider.left = 35; soluteAmountSlider.top = 65; // to the right of the Solute Amount slider solutionVolumeSlider.left = soluteAmountSlider.right + 20; solutionVolumeSlider.y = soluteAmountSlider.y; // to the right of the Solution Volume slider beakerNode.left = solutionVolumeSlider.right + 20; beakerNode.y = soluteAmountSlider.y; // same coordinate frame as beaker solutionNode.x = beakerNode.x; solutionNode.y = beakerNode.y; // same coordinate frame as beaker precipitateNode.x = beakerNode.x; precipitateNode.y = beakerNode.y; // centered below beaker soluteComboBox.centerX = beakerNode.centerX; soluteComboBox.top = beakerNode.bottom + 50; // toward bottom of the beaker var saturatedIndicatorVisible = saturatedIndicator.visible; // so we can layout an invisible node saturatedIndicator.visible = true; saturatedIndicator.centerX = beakerNode.x + ( cylinderSize.width / 2 ); saturatedIndicator.bottom = beakerNode.bottom - ( 0.2 * cylinderSize.height ); saturatedIndicator.visible = saturatedIndicatorVisible; // right of beaker concentrationDisplay.left = beakerNode.right + 50; concentrationDisplay.bottom = beakerNode.bottom; // left of combo box showValuesCheckBox.right = soluteComboBox.left - 50; showValuesCheckBox.centerY = soluteComboBox.centerY; // right of combo box resetAllButton.left = Math.max( soluteComboBox.right + 10, concentrationDisplay.centerX - ( resetAllButton.width / 2 ) ); resetAllButton.centerY = soluteComboBox.centerY; } }
/** * @constructor * * @param {ModelMoleculesModel} model the model for the entire screen */ function MoleculeShapesScreenView( model ) { ScreenView.call( this, { layoutBounds: new Bounds2( 0, 0, 1024, 618 ) } ); var self = this; this.model = model; // @private {ModelMoleculesModel} // our target for drags that don't hit other UI components this.backgroundEventTarget = Rectangle.bounds( this.layoutBounds, {} ); // @private this.addChild( this.backgroundEventTarget ); // updated in layout this.activeScale = 1; // @private scale applied to interaction that isn't directly tied to screen coordinates (rotation) this.screenWidth = null; // @public this.screenHeight = null; // @public // main three.js Scene setup this.threeScene = new THREE.Scene(); // @private this.threeCamera = new THREE.PerspectiveCamera(); // @private will set the projection parameters on layout // @public {THREE.Renderer} this.threeRenderer = MoleculeShapesGlobals.useWebGLProperty.get() ? new THREE.WebGLRenderer( { antialias: true, preserveDrawingBuffer: phet.chipper.queryParameters.preserveDrawingBuffer } ) : new THREE.CanvasRenderer( { devicePixelRatio: 1 // hopefully helps performance a bit } ); this.threeRenderer.setPixelRatio( window.devicePixelRatio || 1 ); // @private {ContextLossFailureDialog|null} - dialog shown on context loss, constructed // lazily because Dialog requires sim bounds during construction this.contextLossDialog = null; // In the event of a context loss, we'll just show a dialog. See https://github.com/phetsims/molecule-shapes/issues/100 if ( MoleculeShapesGlobals.useWebGLProperty.get() ) { this.threeRenderer.context.canvas.addEventListener( 'webglcontextlost', function( event ) { event.preventDefault(); self.showContextLossDialog(); if ( document.domain === 'phet.colorado.edu' ) { window._gaq && window._gaq.push( [ '_trackEvent', 'WebGL Context Loss', 'molecule-shapes ' + phet.joist.sim.version, document.URL ] ); } } ); } MoleculeShapesColorProfile.backgroundProperty.link( function( color ) { self.threeRenderer.setClearColor( color.toNumber(), 1 ); } ); MoleculeShapesScreenView.addLightsToScene( this.threeScene ); this.threeCamera.position.copy( MoleculeShapesScreenView.cameraPosition ); // sets the camera's position // @private add the Canvas in with a DOM node that prevents Scenery from applying transformations on it this.domNode = new DOM( this.threeRenderer.domElement, { preventTransform: true, // Scenery 0.2 override for transformation invalidateDOM: function() { // don't do bounds detection, it's too expensive. We're not pickable anyways this.invalidateSelf( new Bounds2( 0, 0, 0, 0 ) ); }, pickable: false } ); this.domNode.invalidateDOM(); // Scenery 0.1 override for transformation this.domNode.updateCSSTransform = function() {}; // support Scenery/Joist 0.2 screenshot (takes extra work to output) this.domNode.renderToCanvasSelf = function( wrapper ) { var canvas = null; var effectiveWidth = Math.ceil( self.screenWidth ); var effectiveHeight = Math.ceil( self.screenHeight ); // This WebGL workaround is so we can avoid the preserveDrawingBuffer setting that would impact performance. // We render to a framebuffer and extract the pixel data directly, since we can't create another renderer and // share the view (three.js constraint). if ( MoleculeShapesGlobals.useWebGLProperty.get() ) { // set up a framebuffer (target is three.js terminology) to render into var target = new THREE.WebGLRenderTarget( effectiveWidth, effectiveHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBAFormat } ); // render our screen content into the framebuffer self.render( target ); // set up a buffer for pixel data, in the exact typed formats we will need var buffer = new window.ArrayBuffer( effectiveWidth * effectiveHeight * 4 ); var imageDataBuffer = new window.Uint8ClampedArray( buffer ); var pixels = new window.Uint8Array( buffer ); // read the pixel data into the buffer var gl = self.threeRenderer.getContext(); gl.readPixels( 0, 0, effectiveWidth, effectiveHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels ); // create a Canvas with the correct size, and fill it with the pixel data canvas = document.createElement( 'canvas' ); canvas.width = effectiveWidth; canvas.height = effectiveHeight; var tmpContext = canvas.getContext( '2d' ); var imageData = tmpContext.createImageData( effectiveWidth, effectiveHeight ); imageData.data.set( imageDataBuffer ); tmpContext.putImageData( imageData, 0, 0 ); } else { // If just falling back to Canvas, we can directly render out! canvas = self.threeRenderer.domElement; } var context = wrapper.context; context.save(); // Take the pixel ratio into account, see https://github.com/phetsims/molecule-shapes/issues/149 const inverse = 1 / ( window.devicePixelRatio || 1 ); if ( MoleculeShapesGlobals.useWebGLProperty.get() ) { context.setTransform( 1, 0, 0, -1, 0, effectiveHeight ); // no need to take pixel scaling into account } else { context.setTransform( inverse, 0, 0, inverse, 0, 0 ); } context.drawImage( canvas, 0, 0 ); context.restore(); }; this.addChild( this.domNode ); // overlay Scene for bond-angle labels (if WebGL) this.overlayScene = new THREE.Scene(); // @private this.overlayCamera = new THREE.OrthographicCamera(); // @private this.overlayCamera.position.z = 50; // @private this.addChild( new ResetAllButton( { right: this.layoutBounds.maxX - 10, bottom: this.layoutBounds.maxY - 10, listener: function() { model.reset(); } } ) ); this.addChild( new GeometryNamePanel( model, { left: this.layoutBounds.minX + 10, bottom: this.layoutBounds.maxY - 10 } ) ); // we only want to support dragging particles OR rotating the molecule (not both) at the same time var draggedParticleCount = 0; var isRotating = false; var multiDragListener = { down: function( event, trail ) { if ( !event.canStartPress() ) { return; } // if we are already rotating the entire molecule, no more drags can be handled if ( isRotating ) { return; } var dragMode = null; var draggedParticle = null; var pair = self.getElectronPairUnderPointer( event.pointer, !( event.pointer instanceof Mouse ) ); if ( pair && !pair.userControlledProperty.get() ) { // we start dragging that pair group with this pointer, moving it along the sphere where it can exist dragMode = 'pairExistingSpherical'; draggedParticle = pair; pair.userControlledProperty.set( true ); draggedParticleCount++; } else if ( draggedParticleCount === 0 ) { // we don't want to rotate while we are dragging any particles // we rotate the entire molecule with this pointer dragMode = 'modelRotate'; isRotating = true; } else { // can't drag the pair OR rotate the molecule return; } var lastGlobalPoint = event.pointer.point.copy(); event.pointer.cursor = 'pointer'; event.pointer.addInputListener( { // end drag on either up or cancel (not supporting full cancel behavior) up: function( event, trail ) { this.endDrag( event, trail ); }, cancel: function( event, trail ) { this.endDrag( event, trail ); }, move: function( event, trail ) { if ( dragMode === 'modelRotate' ) { var delta = event.pointer.point.minus( lastGlobalPoint ); lastGlobalPoint.set( event.pointer.point ); var scale = 0.007 / self.activeScale; // tuned constant for acceptable drag motion var newQuaternion = new THREE.Quaternion().setFromEuler( new THREE.Euler( delta.y * scale, delta.x * scale, 0 ) ); newQuaternion.multiply( model.moleculeQuaternionProperty.get() ); model.moleculeQuaternionProperty.value = newQuaternion; } else if ( dragMode === 'pairExistingSpherical' ) { if ( _.includes( model.moleculeProperty.get().groups, draggedParticle ) ) { draggedParticle.dragToPosition( self.getSphericalMoleculePosition( event.pointer.point, draggedParticle ) ); } } }, // not a Scenery event endDrag: function( event, trail ) { if ( dragMode === 'pairExistingSpherical' ) { draggedParticle.userControlledProperty.set( false ); draggedParticleCount--; } else if ( dragMode === 'modelRotate' ) { isRotating = false; } event.pointer.removeInputListener( this ); event.pointer.cursor = null; } } ); } }; this.backgroundEventTarget.addInputListener( multiDragListener ); // Consider updating the cursor even if we don't move? (only if we have mouse movement)? Current development // decision is to ignore this edge case in favor of performance. this.backgroundEventTarget.addInputListener( { mousemove: function( event ) { self.backgroundEventTarget.cursor = self.getElectronPairUnderPointer( event.pointer, false ) ? 'pointer' : null; } } ); // update the molecule view's rotation when the model's rotation changes model.moleculeQuaternionProperty.link( function( quaternion ) { // moleculeView is created in the subtype (not yet). will handle initial rotation in addMoleculeView if ( self.moleculeView ) { self.moleculeView.quaternion.copy( quaternion ); self.moleculeView.updateMatrix(); self.moleculeView.updateMatrixWorld(); } } ); // @private - create a pool of angle labels of the desired type this.angleLabels = []; for ( var i = 0; i < 15; i++ ) { if ( MoleculeShapesGlobals.useWebGLProperty.get() ) { this.angleLabels[ i ] = new LabelWebGLView( this.threeRenderer ); this.overlayScene.add( this.angleLabels[ i ] ); this.angleLabels[ i ].unsetLabel(); } else { this.angleLabels[ i ] = new LabelFallbackNode(); this.addChild( this.angleLabels[ i ] ); } } }
/** * Main view of the flow sim. * @param {FlowModel} flowModel of the simulation * @constructor */ function FlowView( flowModel ) { var flowView = this; ScreenView.call( this, { renderer: 'svg' } ); // view co-ordinates (370,140) map to model origin (0,0) with inverted y-axis (y grows up in the model) var modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( Vector2.ZERO, new Vector2( 370, 140 ), 50 ); //1m = 50Px var groundY = modelViewTransform.modelToViewY( 0 ); var backgroundNodeStartX = -5000; var backgroundNodeWidth = 10000; var skyExtensionHeight = 10000; var groundDepth = 10000; // add rectangle on top of the sky node to extend sky upwards. See https://github.com/phetsims/fluid-pressure-and-flow/issues/87 this.addChild( new Rectangle( backgroundNodeStartX, -skyExtensionHeight, backgroundNodeWidth, skyExtensionHeight, { stroke: '#01ACE4', fill: '#01ACE4' } ) ); // add sky node this.addChild( new SkyNode( backgroundNodeStartX, 0, backgroundNodeWidth, groundY, groundY ) ); // add ground node with gradient var groundNode = new GroundNode( backgroundNodeStartX, groundY, backgroundNodeWidth, groundDepth, 400, { topColor: '#9D8B61', bottomColor: '#645A3C' } ); this.addChild( groundNode ); // add grass above the ground var grassPattern = new Pattern( grassImg ).setTransformMatrix( Matrix3.scale( 0.25 ) ); var grassRectYOffset = 1; var grassRectHeight = 10; this.addChild( new Rectangle( backgroundNodeStartX, grassRectYOffset, backgroundNodeWidth, grassRectHeight, { fill: grassPattern, bottom: groundNode.top } ) ); // tools control panel var toolsControlPanel = new ToolsControlPanel( flowModel, { right: this.layoutBounds.right - 7, top: 7 } ); this.addChild( toolsControlPanel ); // units control panel var unitsControlPanel = new UnitsControlPanel( flowModel.measureUnitsProperty, 50, { right: toolsControlPanel.left - 7, top: toolsControlPanel.top } ); this.addChild( unitsControlPanel ); var fluxMeterNode = new FluxMeterNode( flowModel, modelViewTransform, { stroke: 'blue' } ); flowModel.isFluxMeterVisibleProperty.linkAttribute( fluxMeterNode.ellipse2, 'visible' ); //adding pipe Node this.pipeNode = new PipeNode( flowModel, flowModel.pipe, modelViewTransform, this.layoutBounds ); this.addChild( this.pipeNode ); // add the back ellipse of the fluxMeter to the pipe node's pre-particle layer this.pipeNode.preParticleLayer.addChild( fluxMeterNode.ellipse2 ); // now add the front part of the fluxMeter this.addChild( fluxMeterNode ); // add the reset button var resetAllButton = new ResetAllButton( { listener: function() { flowModel.reset(); flowView.pipeNode.reset(); }, radius: 18, bottom: this.layoutBounds.bottom - 7, right: this.layoutBounds.right - 13 } ); this.addChild( resetAllButton ); // add the fluid density control slider var fluidDensityControlNode = new ControlSlider( flowModel.measureUnitsProperty, flowModel.fluidDensityProperty, flowModel.getFluidDensityString.bind( flowModel ), flowModel.fluidDensityRange, flowModel.fluidDensityControlExpandedProperty, { right: resetAllButton.left - 55, bottom: resetAllButton.bottom, title: fluidDensityString, ticks: [ { title: waterString, value: flowModel.fluidDensity }, { title: gasolineString, value: flowModel.fluidDensityRange.min }, { title: honeyString, value: flowModel.fluidDensityRange.max } ], scale: 0.9, titleAlign: 'center' } ); this.addChild( fluidDensityControlNode ); // add the sensors panel var sensorPanel = new Rectangle( 0, 0, 167, 85, 10, 10, { stroke: 'gray', lineWidth: 1, fill: '#f2fa6a', right: unitsControlPanel.left - 4, top: toolsControlPanel.top } ); this.addChild( sensorPanel ); flowModel.isGridInjectorPressedProperty.link( function( isGridInjectorPressed ) { if ( isGridInjectorPressed ) { flowModel.injectGridParticles(); flowView.pipeNode.gridInjectorNode.redButton.enabled = false; } else { flowView.pipeNode.gridInjectorNode.redButton.enabled = true; } } ); // add play pause button and step button var stepButton = new StepButton( function() { flowModel.timer.step( 0.016 ); flowModel.propagateParticles( 0.016 ); }, flowModel.isPlayProperty, { radius: 12, stroke: 'black', fill: '#005566', right: fluidDensityControlNode.left - 82, bottom: this.layoutBounds.bottom - 14 } ); this.addChild( stepButton ); var playPauseButton = new PlayPauseButton( flowModel.isPlayProperty, { radius: 18, stroke: 'black', fill: '#005566', y: stepButton.centerY, right: stepButton.left - inset } ); this.addChild( playPauseButton ); // add sim speed controls var slowMotionRadioBox = new AquaRadioButton( flowModel.speedProperty, 'slow', new Text( slowMotionString, { font: new PhetFont( 12 ) } ), { radius: 8 } ); var normalMotionRadioBox = new AquaRadioButton( flowModel.speedProperty, 'normal', new Text( normalString, { font: new PhetFont( 12 ) } ), { radius: 8 } ); var speedControlMaxWidth = ( slowMotionRadioBox.width > normalMotionRadioBox.width ) ? slowMotionRadioBox.width : normalMotionRadioBox.width; slowMotionRadioBox.touchArea = new Bounds2( slowMotionRadioBox.localBounds.minX, slowMotionRadioBox.localBounds.minY, slowMotionRadioBox.localBounds.minX + speedControlMaxWidth, slowMotionRadioBox.localBounds.maxY ); normalMotionRadioBox.touchArea = new Bounds2( normalMotionRadioBox.localBounds.minX, normalMotionRadioBox.localBounds.minY, normalMotionRadioBox.localBounds.minX + speedControlMaxWidth, normalMotionRadioBox.localBounds.maxY ); var speedControl = new VBox( { align: 'left', spacing: 5, children: [ slowMotionRadioBox, normalMotionRadioBox ] } ); this.addChild( speedControl.mutate( { right: playPauseButton.left - 8, bottom: playPauseButton.bottom } ) ); // add flow rate panel var flowRateControlNode = new ControlSlider( flowModel.measureUnitsProperty, flowModel.pipe.flowRateProperty, flowModel.getFluidFlowRateString.bind( flowModel ), flowModel.flowRateRange, flowModel.flowRateControlExpandedProperty, { right: speedControl.left - 20, bottom: fluidDensityControlNode.bottom, title: flowRateString, ticks: [ { title: 'Min', value: Constants.MIN_FLOW_RATE }, { title: 'Max', value: Constants.MAX_FLOW_RATE } ], ticksVisible: false, titleAlign: 'center', scale: 0.9 } ); this.addChild( flowRateControlNode ); // add speedometers within the sensor panel bounds _.each( flowModel.speedometers, function( velocitySensor ) { velocitySensor.positionProperty.storeInitialValue( new Vector2( sensorPanel.visibleBounds.centerX - 75, sensorPanel.visibleBounds.centerY - 30 ) ); velocitySensor.positionProperty.reset(); this.addChild( new VelocitySensorNode( modelViewTransform, velocitySensor, flowModel.measureUnitsProperty, [ flowModel.pipe.flowRateProperty, flowModel.pipe.frictionProperty ], flowModel.getVelocityAt.bind( flowModel ), sensorPanel.visibleBounds, this.layoutBounds, { scale: 0.9 } ) ); }.bind( this ) ); // add barometers within the sensor panel bounds _.each( flowModel.barometers, function( barometer ) { barometer.positionProperty.storeInitialValue( new Vector2( sensorPanel.visibleBounds.centerX + 50, sensorPanel.visibleBounds.centerY - 10 ) ); barometer.reset(); this.addChild( new BarometerNode( modelViewTransform, barometer, flowModel.measureUnitsProperty, [ flowModel.fluidDensityProperty, flowModel.pipe.flowRateProperty, flowModel.pipe.frictionProperty ], flowModel.getPressureAtCoords.bind( flowModel ), flowModel.getPressureString.bind( flowModel ), sensorPanel.visibleBounds, this.layoutBounds, { minPressure: Constants.MIN_PRESSURE, maxPressure: Constants.MAX_PRESSURE, scale: 0.9 } ) ); }.bind( this ) ); // add the rule node this.addChild( new FPAFRuler( flowModel.isRulerVisibleProperty, flowModel.rulerPositionProperty, flowModel.measureUnitsProperty, modelViewTransform, this.layoutBounds ) ); }
function MemoryTestsView() { ScreenView.call( this ); }
/** * Constructor for the screen view of Molecules and Light. * * @param {PhotonAbsorptionModel} photonAbsorptionModel * @param {Tandem} tandem - support for exporting instances from the sim * @constructor */ function MoleculesAndLightScreenView( photonAbsorptionModel, tandem ) { ScreenView.call( this, { layoutBounds: new Bounds2( 0, 0, 768, 504 ) } ); var modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( Vector2.ZERO, new Vector2( Math.round( INTERMEDIATE_RENDERING_SIZE.width * 0.55 ), Math.round( INTERMEDIATE_RENDERING_SIZE.height * 0.50 ) ), 0.10 ); // Scale factor - Smaller number zooms out, bigger number zooms in. // Create the observation window. This will hold all photons, molecules, and photonEmitters for this photon // absorption model. var observationWindow = new ObservationWindow( photonAbsorptionModel, modelViewTransform, tandem ); this.addChild( observationWindow ); // This rectangle hides photons that are outside the observation window. // TODO: This rectangle is a temporary workaround that replaces the clipping area in ObservationWindow because of a // Safari specific SVG bug caused by clipping. See https://github.com/phetsims/molecules-and-light/issues/105 and // https://github.com/phetsims/scenery/issues/412. var clipRectangle = new Rectangle( observationWindow.bounds.copy().dilate( 4 * FRAME_LINE_WIDTH ), CORNER_RADIUS, CORNER_RADIUS, { stroke: '#C5D6E8', lineWidth: 8 * FRAME_LINE_WIDTH } ); this.addChild( clipRectangle ); // Create the window frame node that borders the observation window. var windowFrameNode = new WindowFrameNode( observationWindow, '#BED0E7', '#4070CE' ); this.addChild( windowFrameNode ); // Set positions of the observation window and window frame. observationWindow.translate( OBSERVATION_WINDOW_LOCATION ); clipRectangle.translate( OBSERVATION_WINDOW_LOCATION ); windowFrameNode.translate( OBSERVATION_WINDOW_LOCATION ); // Create the control panel for photon emission frequency. var photonEmissionControlPanel = new QuadEmissionFrequencyControlPanel( photonAbsorptionModel, tandem ); photonEmissionControlPanel.leftTop = ( new Vector2( OBSERVATION_WINDOW_LOCATION.x, 350 ) ); // Create the molecule control panel var moleculeControlPanel = new MoleculeSelectionPanel( photonAbsorptionModel, tandem ); moleculeControlPanel.leftTop = ( new Vector2( 530, windowFrameNode.top ) ); // Add reset all button. var resetAllButton = new ResetAllButton( { listener: function() { photonAbsorptionModel.reset(); }, bottom: this.layoutBounds.bottom - 15, right: this.layoutBounds.right - 15, radius: 18, tandem: tandem.createTandem( 'resetAllButton' ) } ); this.addChild( resetAllButton ); // Add play/pause button. var playPauseButton = new PlayPauseButton( photonAbsorptionModel.playProperty, { bottom: moleculeControlPanel.bottom + 60, centerX: moleculeControlPanel.centerX - 25, radius: 23, tandem: tandem.createTandem( 'playPauseButton' ) } ); this.addChild( playPauseButton ); // Add step button to manually step the animation. var stepButton = new StepButton( function() { photonAbsorptionModel.manualStep(); }, photonAbsorptionModel.playProperty, { centerY: playPauseButton.centerY, centerX: moleculeControlPanel.centerX + 25, radius: 15, tandem: tandem.createTandem( 'stepButton' ) } ); this.addChild( stepButton ); // Window that displays the EM spectrum upon request. Constructed once here so that time is not waisted // drawing a new spectrum window every time the user presses the 'Show Light Spectrum' button. var spectrumWindow = new SpectrumWindow( tandem.createTandem( 'spectrumWindow' ) ); // Add the button for displaying the electromagnetic spectrum. Scale down the button content when it gets too // large. This is done to support translations. Max width of this button is the width of the molecule control // panel minus twice the default x margin of a rectangular push button. var buttonContent = new Text( buttonCaptionString, { font: new PhetFont( 18 ) } ); if ( buttonContent.width > moleculeControlPanel.width - 16 ) { buttonContent.scale( (moleculeControlPanel.width - 16 ) / buttonContent.width ); } var showSpectrumButton = new RectangularPushButton( { content: buttonContent, baseColor: 'rgb(98, 173, 205)', listener: function() { spectrumWindow.show(); }, tandem: tandem.createTandem( 'showLightSpectrumButton' ) } ); showSpectrumButton.center = ( new Vector2( moleculeControlPanel.centerX, photonEmissionControlPanel.centerY - 33 ) ); this.addChild( showSpectrumButton ); // Add the nodes in the order necessary for correct layering. this.addChild( photonEmissionControlPanel ); this.addChild( moleculeControlPanel ); }
/** * @param {CustomModel} model * @param {ModelViewTransform2} modelViewTransform * @constructor */ function CustomView( model, modelViewTransform ) { var thisView = this; ScreenView.call( thisView, { renderer: 'svg' } ); // view-specific properties var viewProperties = new PropertySet( { ratioVisible: false, moleculeCountVisible: false, pHMeterExpanded: true, graphExpanded: true } ); // beaker var beakerNode = new BeakerNode( model.beaker, modelViewTransform ); var solutionNode = new SolutionNode( model.solution, model.beaker, modelViewTransform ); var volumeIndicatorNode = new VolumeIndicatorNode( model.solution.volumeProperty, model.beaker, modelViewTransform ); // 'H3O+/OH- ratio' representation var ratioNode = new RatioNode( model.beaker, model.solution, modelViewTransform, { visible: viewProperties.ratioVisibleProperty.get() } ); viewProperties.ratioVisibleProperty.linkAttribute( ratioNode, 'visible' ); // 'molecule count' representation var moleculeCountNode = new MoleculeCountNode( model.solution ); viewProperties.moleculeCountVisibleProperty.linkAttribute( moleculeCountNode, 'visible' ); // beaker controls var beakerControls = new BeakerControls( viewProperties.ratioVisibleProperty, viewProperties.moleculeCountVisibleProperty ); // graph var graphNode = new GraphNode( model.solution, viewProperties.graphExpandedProperty, { isInteractive: true, logScaleHeight: 565 } ); // pH meter var pHMeterTop = 15; var pHMeterNode = new PHMeterNode( model.solution, modelViewTransform.modelToViewY( model.beaker.location.y ) - pHMeterTop, viewProperties.pHMeterExpandedProperty, { attachProbe: 'right', isInteractive: true } ); var resetAllButton = new ResetAllButton( { scale: 1.32, listener: function() { model.reset(); viewProperties.reset(); graphNode.reset(); } } ); // Parent for all nodes added to this screen var rootNode = new Node( { children: [ // nodes are rendered in this order solutionNode, pHMeterNode, ratioNode, beakerNode, moleculeCountNode, volumeIndicatorNode, beakerControls, graphNode, resetAllButton ] } ); thisView.addChild( rootNode ); // Layout of nodes that don't have a location specified in the model pHMeterNode.left = beakerNode.left; pHMeterNode.top = pHMeterTop; moleculeCountNode.centerX = beakerNode.centerX; moleculeCountNode.bottom = beakerNode.bottom - 25; beakerControls.centerX = beakerNode.centerX; beakerControls.top = beakerNode.bottom + 10; graphNode.right = beakerNode.left - 70; graphNode.top = pHMeterNode.top; resetAllButton.right = this.layoutBounds.right - 40; resetAllButton.bottom = this.layoutBounds.bottom - 20; }
/** * Constructor. * * @param {BAAGameModel} gameModel * @param {Tandem} tandem * @constructor */ function BAAGameScreenView( gameModel, tandem ) { ScreenView.call( this, { layoutBounds: ShredConstants.LAYOUT_BOUNDS, tandem: tandem } ); var self = this; // Add a root node where all of the game-related nodes will live. var rootNode = new Node(); self.addChild( rootNode ); var startGameLevelNode = new StartGameLevelNode( gameModel, this.layoutBounds, tandem.createTandem( 'startGameLevelNode' ) ); var scoreboard = new FiniteStatusBar( this.layoutBounds, this.visibleBoundsProperty, gameModel.scoreProperty, { challengeIndexProperty: gameModel.challengeIndexProperty, numberOfChallengesProperty: new Property( BAAGameModel.CHALLENGES_PER_LEVEL ), elapsedTimeProperty: gameModel.elapsedTimeProperty, timerEnabledProperty: gameModel.timerEnabledProperty, barFill: 'rgb( 49, 117, 202 )', textFill: 'white', xMargin: 20, dynamicAlignment: false, levelVisible: false, challengeNumberVisible: false, startOverButtonOptions: { font: new PhetFont( 20 ), textFill: 'black', baseColor: '#e5f3ff', xMargin: 6, yMargin: 5, listener: function() { gameModel.newGame(); } }, tandem: tandem.createTandem( 'scoreboard' ) } ); scoreboard.centerX = this.layoutBounds.centerX; scoreboard.top = 0; var gameAudioPlayer = new GameAudioPlayer( gameModel.soundEnabledProperty ); this.rewardNode = null; this.levelCompletedNode = null; // @private // Monitor the game state and update the view accordingly. gameModel.stateProperty.link( function( state, previousState ) { ( previousState && previousState.disposeEmitter ) && previousState.disposeEmitter.emit(); if ( state === BAAGameState.CHOOSING_LEVEL ) { rootNode.removeAllChildren(); rootNode.addChild( startGameLevelNode ); if ( self.rewardNode !== null ) { self.rewardNode.dispose(); } if ( self.levelCompletedNode !== null ) { self.levelCompletedNode.dispose(); } self.rewardNode = null; self.levelCompletedNode = null; } else if ( state === BAAGameState.LEVEL_COMPLETED ) { rootNode.removeAllChildren(); if ( gameModel.scoreProperty.get() === BAAGameModel.MAX_POINTS_PER_GAME_LEVEL || BAAQueryParameters.reward ) { // Perfect score, add the reward node. self.rewardNode = new BAARewardNode( tandem.createTandem( 'rewardNode' ) ); rootNode.addChild( self.rewardNode ); // Play the appropriate audio feedback gameAudioPlayer.gameOverPerfectScore(); } else if ( gameModel.scoreProperty.get() > 0 ) { gameAudioPlayer.gameOverImperfectScore(); } if ( gameModel.provideFeedbackProperty.get() ) { // Add the dialog node that indicates that the level has been completed. self.levelCompletedNode = new LevelCompletedNode( gameModel.levelProperty.get() + 1, gameModel.scoreProperty.get(), BAAGameModel.MAX_POINTS_PER_GAME_LEVEL, BAAGameModel.CHALLENGES_PER_LEVEL, gameModel.timerEnabledProperty.get(), gameModel.elapsedTimeProperty.get(), gameModel.bestTimes[ gameModel.levelProperty.get() ].value, gameModel.newBestTime, function() { gameModel.stateProperty.set( BAAGameState.CHOOSING_LEVEL ); }, { centerX: self.layoutBounds.width / 2, centerY: self.layoutBounds.height / 2, levelVisible: false, maxWidth: self.layoutBounds.width, tandem: tandem.createTandem( 'levelCompletedNode' ) } ); rootNode.addChild( self.levelCompletedNode ); } } else if ( typeof( state.createView ) === 'function' ) { // Since we're not in the start or game-over states, we must be // presenting a challenge. rootNode.removeAllChildren(); var challengeView = state.createView( self.layoutBounds, tandem.createTandem( state.tandem.tail + 'View' ) ); state.disposeEmitter.addListener( function disposeListener() { challengeView.dispose(); state.disposeEmitter.removeListener( disposeListener ); } ); rootNode.addChild( challengeView ); rootNode.addChild( scoreboard ); } } ); }
/** * @param {TugOfWarModel} model * @constructor */ function TugOfWarView( model ) { ScreenView.call( this, {renderer: 'svg', layoutBounds: LAYOUT_BOUNDS} ); //Fit to the window and render the initial scene var width = this.layoutBounds.width; var height = this.layoutBounds.height; var tugOfWarView = this; this.model = model; //Create the sky and ground. Allow the sky and ground to go off the screen in case the window is larger than the sim aspect ratio var skyHeight = 376; var grassY = 368; var groundHeight = height - skyHeight; this.addChild( new Rectangle( -width, -skyHeight, width * 3, skyHeight * 2, {fill: new LinearGradient( 0, 0, 0, skyHeight ).addColorStop( 0, '#02ace4' ).addColorStop( 1, '#cfecfc' )} ) ); this.addChild( new Rectangle( -width, skyHeight, width * 3, groundHeight * 3, { fill: '#c59a5b'} ) ); //Show the grass. this.addChild( new Image( grassImage, {x: 13, y: grassY} ) ); this.addChild( new Image( grassImage, {x: 13 - grassImage.width, y: grassY} ) ); this.addChild( new Image( grassImage, {x: 13 + grassImage.width, y: grassY} ) ); this.cartNode = new Image( cartImage, {y: 221} ); //Black caret below the cart this.addChild( new Path( new Shape().moveTo( -10, 10 ).lineTo( 0, 0 ).lineTo( 10, 10 ), { stroke: '#000000', lineWidth: 3, x: this.layoutBounds.width / 2, y: grassY + 10} ) ); //Add toolbox backgrounds for the pullers var toolboxHeight = 216; this.addChild( new Rectangle( 25, this.layoutBounds.height - toolboxHeight - 4, 324, toolboxHeight, 10, 10, {fill: '#e7e8e9', stroke: '#000000', lineWidth: 1} ) ); this.addChild( new Rectangle( 630, this.layoutBounds.height - toolboxHeight - 4, 324, toolboxHeight, 10, 10, { fill: '#e7e8e9', stroke: '#000000', lineWidth: 1} ) ); //Split into another canvas to speed up rendering this.addChild( new Node( {layerSplit: true} ) ); //Create the arrow nodes var opacity = 0.8; this.sumArrow = new ReadoutArrow( sumOfForcesString, '#7dc673', this.layoutBounds.width / 2, 100, this.model.netForceProperty, this.model.showValuesProperty, {lineDash: [ 10, 5 ], labelPosition: 'top', opacity: opacity} ); this.leftArrow = new ReadoutArrow( leftForceString, '#bf8b63', this.layoutBounds.width / 2, 200, this.model.leftForceProperty, this.model.showValuesProperty, {lineDash: [ 10, 5], labelPosition: 'side', opacity: opacity} ); this.rightArrow = new ReadoutArrow( rightForceString, '#bf8b63', this.layoutBounds.width / 2, 200, this.model.rightForceProperty, this.model.showValuesProperty, {lineDash: [ 10, 5], labelPosition: 'side', opacity: opacity} ); //Arrows should be dotted when the sim is paused, but solid after pressing 'go' this.model.runningProperty.link( function( running ) { [tugOfWarView.sumArrow, tugOfWarView.leftArrow, tugOfWarView.rightArrow].forEach( function( arrow ) { arrow.setArrowDash( running ? null : [ 10, 5 ] ); } ); } ); this.model.showSumOfForcesProperty.linkAttribute( this.sumArrow, 'visible' ); this.ropeNode = new Image( ropeImage, {x: 51, y: 273 } ); model.knots.forEach( function( knot ) { tugOfWarView.addChild( new KnotHighlightNode( knot ) ); } ); this.addChild( this.ropeNode ); this.model.cart.xProperty.link( function( x ) { tugOfWarView.cartNode.x = x + 412; tugOfWarView.ropeNode.x = x + 51; } ); this.addChild( this.cartNode ); //Add the go button, but only if there is a puller attached var goPauseButton = new GoPauseButton( this.model, this.layoutBounds.width ); var goPauseButtonContainer = new Node( {children: [goPauseButton]} ); this.addChild( goPauseButtonContainer ); //Return button this.addChild( new ReturnButton( model, {centerX: this.layoutBounds.centerX, top: goPauseButton.bottom + 5} ) ); //Lookup a puller image given a puller instance and whether they are leaning or not. var getPullerImage = function( puller, leaning ) { var type = puller.type; var size = puller.size; //todo: compress with more ternary? return type === 'blue' && size === 'large' && !leaning ? pullFigureLargeBlue0Image : type === 'blue' && size === 'large' && leaning ? pullFigureLargeBlue3Image : type === 'blue' && size === 'medium' && !leaning ? pullFigureBlue0Image : type === 'blue' && size === 'medium' && leaning ? pullFigureBlue3Image : type === 'blue' && size === 'small' && !leaning ? pullFigureSmallBlue0Image : type === 'blue' && size === 'small' && leaning ? pullFigureSmallBlue3Image : type === 'red' && size === 'large' && !leaning ? pullFigureLargeRed0Image : type === 'red' && size === 'large' && leaning ? pullFigureLargeRed3Image : type === 'red' && size === 'medium' && !leaning ? pullFigureRed0Image : type === 'red' && size === 'medium' && leaning ? pullFigureRed3Image : type === 'red' && size === 'small' && !leaning ? pullFigureSmallRed0Image : type === 'red' && size === 'small' && leaning ? pullFigureSmallRed3Image : null; }; var pullerLayer = new Node(); this.addChild( pullerLayer ); this.model.pullers.forEach( function( puller ) { pullerLayer.addChild( new PullerNode( puller, tugOfWarView.model, getPullerImage( puller, false ), getPullerImage( puller, true ) ) ); } ); //Add the arrow nodes after the pullers so they will appear in the front in z-ordering this.addChild( this.leftArrow ); this.addChild( this.rightArrow ); this.addChild( this.sumArrow ); //Show the control panel this.addChild( new TugOfWarControlPanel( this.model ).mutate( {right: 981 - 5, top: 5} ) ); //Show the flag node when pulling is complete var showFlagNode = function() { tugOfWarView.addChild( new FlagNode( model, tugOfWarView.layoutBounds.width / 2, 10 ) ); }; model.stateProperty.link( function( state ) { if ( state === 'completed' ) { showFlagNode(); } } ); //Accessibility for reading out the total force var textProperty = new Property( '' ); model.numberPullersAttachedProperty.link( function() { textProperty.value = 'Left force: ' + Math.abs( model.getLeftForce() ) + ' Newtons, ' + 'Right force: ' + Math.abs( model.getRightForce() ) + ' Newtons, ' + 'Net Force: ' + Math.abs( model.getNetForce() ) + ' Newtons ' + (model.getNetForce() === 0 ? '' : model.getNetForce() > 0 ? 'to the right' : 'to the left'); } ); this.addLiveRegion( textProperty ); var golfClap = new Sound( golfClapSound ); //Play audio golf clap when game completed model.stateProperty.link( function( state ) { if ( state === 'completed' && model.volumeOn ) { golfClap.play(); } } ); //Show 'Sum of Forces = 0' when showForces is selected but the force is zero this.sumOfForcesText = new Text( sumOfForcesEqualsZeroString, {font: new PhetFont( { size: 16, weight: 'bold' } ), centerX: width / 2, y: 53} ); model.multilink( ['netForce', 'showSumOfForces'], function( netForce, showSumOfForces ) {tugOfWarView.sumOfForcesText.visible = !netForce && showSumOfForces;} ); this.addChild( this.sumOfForcesText ); }
var NUM_NUCLEON_LAYERS = 5; // This is based on max number of particles, may need adjustment if that changes. /** * @param {BuildAnAtomModel} model * @param {Tandem} tandem * @constructor */ function AtomView( model, tandem ) { ScreenView.call( this, { layoutBounds: ShredConstants.LAYOUT_BOUNDS, tandem: tandem } ); var self = this; this.model = model; this.resetFunctions = []; // @protected this.periodicTableAccordionBoxExpandedProperty = new BooleanProperty( true, { tandem: tandem.createTandem( 'periodicTableAccordionBoxExpandedProperty' ) } ); // Create the model-view transform. var modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( Vector2.ZERO, new Vector2( self.layoutBounds.width * 0.3, self.layoutBounds.height * 0.45 ), 1.0 ); // Add the node that shows the textual labels, the electron shells, and the center X marker. var atomNode = new AtomNode( model.particleAtom, modelViewTransform, { showElementNameProperty: model.showElementNameProperty, showNeutralOrIonProperty: model.showNeutralOrIonProperty, showStableOrUnstableProperty: model.showStableOrUnstableProperty, electronShellDepictionProperty: model.electronShellDepictionProperty, tandem: tandem.createTandem( 'atomNode' ) } ); this.addChild( atomNode ); // Add the bucket holes. Done separately from the bucket front for layering. _.each( model.buckets, function( bucket ) { self.addChild( new BucketHole( bucket, modelViewTransform, { pickable: false, tandem: tandem.createTandem( bucket.sphereBucketTandem.tail + 'Hole' ) } ) ); } ); // add the layer where the nucleons and electrons will go, this is added last so that it remains on top var nucleonElectronLayer = new Node( { tandem: tandem.createTandem( 'nucleonElectronLayer' ) } ); // Add the layers where the nucleons will exist. var nucleonLayers = []; var nucleonLayersTandem = tandem.createGroupTandem( 'nucleonLayers' ); _.times( NUM_NUCLEON_LAYERS, function() { var nucleonLayer = new Node( { tandem: nucleonLayersTandem.createNextTandem() } ); nucleonLayers.push( nucleonLayer ); nucleonElectronLayer.addChild( nucleonLayer ); } ); nucleonLayers.reverse(); // Set up the nucleon layers so that layer 0 is in front. // Add the layer where the electrons will exist. var electronLayer = new Node( { layerSplit: true, tandem: tandem.createTandem( 'electronLayer' ) } ); nucleonElectronLayer.addChild( electronLayer ); // Add the nucleon particle views. var nucleonsGroupTandem = tandem.createGroupTandem( 'nucleons' ); var electronsGroupTandem = tandem.createGroupTandem( 'electrons' ); // add the nucleons var particleDragBounds = modelViewTransform.viewToModelBounds( this.layoutBounds ); model.nucleons.forEach( function( nucleon ) { nucleonLayers[ nucleon.zLayerProperty.get() ].addChild( new ParticleView( nucleon, modelViewTransform, { dragBounds: particleDragBounds, tandem: nucleonsGroupTandem.createNextTandem() } ) ); // Add a listener that adjusts a nucleon's z-order layering. nucleon.zLayerProperty.link( function( zLayer ) { assert && assert( nucleonLayers.length > zLayer, 'zLayer for nucleon exceeds number of layers, max number may need increasing.' ); // Determine whether nucleon view is on the correct layer. var onCorrectLayer = false; nucleonLayers[ zLayer ].children.forEach( function( particleView ) { if ( particleView.particle === nucleon ) { onCorrectLayer = true; } } ); if ( !onCorrectLayer ) { // Remove particle view from its current layer. var particleView = null; for ( var layerIndex = 0; layerIndex < nucleonLayers.length && particleView === null; layerIndex++ ) { for ( var childIndex = 0; childIndex < nucleonLayers[ layerIndex ].children.length; childIndex++ ) { if ( nucleonLayers[ layerIndex ].children[ childIndex ].particle === nucleon ) { particleView = nucleonLayers[ layerIndex ].children[ childIndex ]; nucleonLayers[ layerIndex ].removeChildAt( childIndex ); break; } } } // Add the particle view to its new layer. assert && assert( particleView !== null, 'Particle view not found during relayering' ); nucleonLayers[ zLayer ].addChild( particleView ); } } ); } ); // Add the electron particle views. model.electrons.forEach( function( electron ) { electronLayer.addChild( new ParticleView( electron, modelViewTransform, { dragBounds: particleDragBounds, tandem: electronsGroupTandem.createNextTandem() } ) ); } ); // When the electrons are represented as a cloud, the individual particles become invisible when added to the atom. var updateElectronVisibility = function() { electronLayer.getChildren().forEach( function( electronNode ) { electronNode.visible = model.electronShellDepictionProperty.get() === 'orbits' || !model.particleAtom.electrons.contains( electronNode.particle ); } ); }; model.particleAtom.electrons.lengthProperty.link( updateElectronVisibility ); model.electronShellDepictionProperty.link( updateElectronVisibility ); // Add the front portion of the buckets. This is done separately from the bucket holes for layering purposes. var bucketFrontLayer = new Node( { tandem: tandem.createTandem( 'bucketFrontLayer' ) } ); _.each( model.buckets, function( bucket ) { var bucketFront = new BucketFront( bucket, modelViewTransform, { tandem: tandem.createTandem( bucket.sphereBucketTandem.tail + 'Front' ) } ); bucketFrontLayer.addChild( bucketFront ); bucketFront.addInputListener( new BucketDragHandler( bucket, bucketFront, modelViewTransform, { tandem: tandem.createTandem( bucket.sphereBucketTandem.tail + 'DragHandler' ) } ) ); } ); // Add the particle count indicator. var particleCountDisplay = new ParticleCountDisplay( model.particleAtom, 13, 250, { tandem: tandem.createTandem( 'particleCountDisplay' ) } ); // Width arbitrarily chosen. this.addChild( particleCountDisplay ); // Add the periodic table display inside of an accordion box. var periodicTableAndSymbol = new PeriodicTableAndSymbol( model.particleAtom, tandem.createTandem( 'periodicTableAndSymbol' ), { pickable: false } ); periodicTableAndSymbol.scale( 0.55 ); // Scale empirically determined to match layout in design doc. var periodicTableAccordionBoxTandem = tandem.createTandem( 'periodicTableAccordionBox' ); this.periodicTableAccordionBox = new AccordionBox( periodicTableAndSymbol, { cornerRadius: 3, titleNode: new Text( elementString, { font: ShredConstants.ACCORDION_BOX_TITLE_FONT, maxWidth: ShredConstants.ACCORDION_BOX_TITLE_MAX_WIDTH, tandem: periodicTableAccordionBoxTandem.createTandem( 'title' ) } ), fill: ShredConstants.DISPLAY_PANEL_BACKGROUND_COLOR, contentAlign: 'left', titleAlignX: 'left', buttonAlign: 'right', expandedProperty: this.periodicTableAccordionBoxExpandedProperty, expandCollapseButtonOptions: { touchAreaXDilation: 8, touchAreaYDilation: 8 }, // phet-io tandem: periodicTableAccordionBoxTandem, // a11y labelContent: elementString } ); this.addChild( this.periodicTableAccordionBox ); var labelVisibilityControlPanelTandem = tandem.createTandem( 'labelVisibilityControlPanel' ); var labelVisibilityControlPanel = new Panel( new VerticalCheckboxGroup( [ { node: new Text( elementString, { font: LABEL_CONTROL_FONT, maxWidth: LABEL_CONTROL_MAX_WIDTH, tandem: labelVisibilityControlPanelTandem.createTandem( 'elementText' ) } ), property: model.showElementNameProperty, tandem: labelVisibilityControlPanelTandem.createTandem( 'showElementNameCheckbox' ) }, { node: new Text( neutralSlashIonString, { font: LABEL_CONTROL_FONT, maxWidth: LABEL_CONTROL_MAX_WIDTH, tandem: labelVisibilityControlPanelTandem.createTandem( 'neutralOrIonText' ) } ), property: model.showNeutralOrIonProperty, tandem: labelVisibilityControlPanelTandem.createTandem( 'showNeutralOrIonCheckbox' ) }, { node: new Text( stableSlashUnstableString, { font: LABEL_CONTROL_FONT, maxWidth: LABEL_CONTROL_MAX_WIDTH, tandem: labelVisibilityControlPanelTandem.createTandem( 'stableUnstableText' ) } ), property: model.showStableOrUnstableProperty, tandem: labelVisibilityControlPanelTandem.createTandem( 'showStableOrUnstableCheckbox' ) } ], { checkboxOptions: { boxWidth: 12 }, spacing: 8, tandem: tandem.createTandem( 'labelVisibilityCheckboxGroup' ) } ), { fill: 'rgb( 245, 245, 245 )', lineWidth: LABEL_CONTROL_LINE_WIDTH, xMargin: 7.5, cornerRadius: 5, resize: false, tandem: labelVisibilityControlPanelTandem } ); var numDividerLines = 2; var dividerLineShape = new Shape().moveTo( 0, 0 ).lineTo( labelVisibilityControlPanel.width - 2 * LABEL_CONTROL_LINE_WIDTH, 0 ); for ( var dividerLines = 0; dividerLines < numDividerLines; dividerLines++ ) { var dividerLine1 = new Path( dividerLineShape, { lineWidth: 1, stroke: 'gray', centerY: labelVisibilityControlPanel.height * ( dividerLines + 1 ) / ( numDividerLines + 1 ), x: LABEL_CONTROL_LINE_WIDTH / 2 } ); labelVisibilityControlPanel.addChild( dividerLine1 ); } this.addChild( labelVisibilityControlPanel ); var labelVisibilityControlPanelTitle = new Text( showString, { font: new PhetFont( { size: 16, weight: 'bold' } ), maxWidth: labelVisibilityControlPanel.width, tandem: tandem.createTandem( 'labelVisibilityControlPanelTitle' ) } ); this.addChild( labelVisibilityControlPanelTitle ); // Add the radio buttons that control the electron representation in the atom. var radioButtonRadius = 6; var orbitsRadioButtonTandem = tandem.createTandem( 'orbitsRadioButton' ); var orbitsRadioButton = new AquaRadioButton( model.electronShellDepictionProperty, 'orbits', new Text( orbitsString, { font: ELECTRON_VIEW_CONTROL_FONT, maxWidth: ELECTRON_VIEW_CONTROL_MAX_WIDTH, tandem: orbitsRadioButtonTandem.createTandem( 'orbitsText' ) } ), { radius: radioButtonRadius, tandem: orbitsRadioButtonTandem } ); var cloudRadioButtonTandem = tandem.createTandem( 'cloudRadioButton' ); var cloudRadioButton = new AquaRadioButton( model.electronShellDepictionProperty, 'cloud', new Text( cloudString, { font: ELECTRON_VIEW_CONTROL_FONT, maxWidth: ELECTRON_VIEW_CONTROL_MAX_WIDTH, tandem: cloudRadioButtonTandem.createTandem( 'cloudText' ) } ), { radius: radioButtonRadius, tandem: cloudRadioButtonTandem } ); var electronViewButtonGroup = new Node( { tandem: tandem.createTandem( 'electronViewButtonGroup' ) } ); electronViewButtonGroup.addChild( new Text( modelString, { font: new PhetFont( { size: 14, weight: 'bold' } ), maxWidth: ELECTRON_VIEW_CONTROL_MAX_WIDTH + 20, tandem: tandem.createTandem( 'electronViewButtonGroupLabel' ) } ) ); orbitsRadioButton.top = electronViewButtonGroup.bottom + 5; orbitsRadioButton.left = electronViewButtonGroup.left; electronViewButtonGroup.addChild( orbitsRadioButton ); cloudRadioButton.top = electronViewButtonGroup.bottom + 5; cloudRadioButton.left = electronViewButtonGroup.left; electronViewButtonGroup.addChild( cloudRadioButton ); this.addChild( electronViewButtonGroup ); // Add the reset button. var resetAllButton = new ResetAllButton( { listener: function() { self.model.reset(); self.reset(); }, right: this.layoutBounds.maxX - CONTROLS_INSET, bottom: this.layoutBounds.maxY - CONTROLS_INSET, radius: BAASharedConstants.RESET_BUTTON_RADIUS, tandem: tandem.createTandem( 'resetAllButton' ) } ); this.addChild( resetAllButton ); // Do the layout. particleCountDisplay.top = CONTROLS_INSET; particleCountDisplay.left = CONTROLS_INSET; this.periodicTableAccordionBox.top = CONTROLS_INSET; this.periodicTableAccordionBox.right = this.layoutBounds.maxX - CONTROLS_INSET; labelVisibilityControlPanel.left = this.periodicTableAccordionBox.left; labelVisibilityControlPanel.bottom = this.layoutBounds.height - CONTROLS_INSET; labelVisibilityControlPanelTitle.bottom = labelVisibilityControlPanel.top; labelVisibilityControlPanelTitle.centerX = labelVisibilityControlPanel.centerX; electronViewButtonGroup.left = atomNode.right + 30; electronViewButtonGroup.bottom = atomNode.bottom + 5; // Any other objects added by class calling it will be added in this node for layering purposes this.controlPanelLayer = new Node( { tandem: tandem.createTandem( 'controlPanelLayer' ) } ); this.addChild( this.controlPanelLayer ); this.addChild( nucleonElectronLayer ); this.addChild( bucketFrontLayer ); }
/** * @constructor */ function LevelSelectionScreenView() { ScreenView.call( this ); var scoreProperty = new Property( 0 ); var bestTimeProperty = new Property( 0 ); var bestTimeVisibleProperty = new BooleanProperty( true ); // Various options for displaying score. var scoreDisplays = new VBox( { resize: false, spacing: 20, align: 'left', centerX: this.layoutBounds.centerX, top: this.layoutBounds.top + 20, children: [ new ScoreDisplayStars( scoreProperty, { numberOfStars: NUM_STARS, perfectScore: SCORE_RANGE.max } ), new ScoreDisplayLabeledStars( scoreProperty, { numberOfStars: NUM_STARS, perfectScore: SCORE_RANGE.max } ), new ScoreDisplayNumberAndStar( scoreProperty ), new ScoreDisplayLabeledNumber( scoreProperty ) ] } ); this.addChild( scoreDisplays ); // Level selection buttons var buttonIcon = new Rectangle( 0, 0, 100, 100, { fill: 'red', stroke: 'black' } ); var buttonWithStars = new LevelSelectionButton( buttonIcon, scoreProperty, { scoreDisplayConstructor: ScoreDisplayStars, scoreDisplayOptions: { numberOfStars: NUM_STARS, perfectScore: SCORE_RANGE.max }, listener: function() { console.log( 'level start' ); } } ); var buttonWithTextAndStars = new LevelSelectionButton( buttonIcon, scoreProperty, { scoreDisplayConstructor: ScoreDisplayLabeledStars, scoreDisplayOptions: { numberOfStars: NUM_STARS, perfectScore: SCORE_RANGE.max }, listener: function() { console.log( 'level start' ); } } ); var buttonWithNumberAndStar = new LevelSelectionButton( buttonIcon, scoreProperty, { scoreDisplayConstructor: ScoreDisplayNumberAndStar, listener: function() { console.log( 'level start' ); } } ); var buttonWithTextAndNumber = new LevelSelectionButton( buttonIcon, scoreProperty, { scoreDisplayConstructor: ScoreDisplayLabeledNumber, listener: function() { console.log( 'level start' ); }, bestTimeProperty: bestTimeProperty, bestTimeVisibleProperty: bestTimeVisibleProperty } ); var levelSelectionButtons = new HBox( { spacing: 20, align: 'top', centerX: this.layoutBounds.centerX, top: scoreDisplays.bottom + 60, children: [ buttonWithStars, buttonWithTextAndStars, buttonWithNumberAndStar, buttonWithTextAndNumber ] } ); this.addChild( levelSelectionButtons ); // Controls for Properties var scoreSlider = new HBox( { children: [ new Text( 'Score: ', { font: new PhetFont( 20 ) } ), new HSlider( scoreProperty, SCORE_RANGE ) ] } ); var bestTimeSlider = new HBox( { children: [ new Text( 'Best Time: ', { font: new PhetFont( 20 ) } ), new HSlider( bestTimeProperty, BEST_TIME_RANGE ) ] } ); var bestTimeVisibleCheckbox = new Checkbox( new Text( 'Best time visible', { font: new PhetFont( 20 ) } ), bestTimeVisibleProperty ); var controls = new HBox( { resize: false, spacing: 30, centerX: this.layoutBounds.centerX, top: levelSelectionButtons.bottom + 60, children: [ scoreSlider, bestTimeSlider, bestTimeVisibleCheckbox ] } ); this.addChild( controls ); }
/** * @param {EnergySkateParkBasicsModel} model * @param {Object} [options] * @constructor */ function EnergySkateParkBasicsScreenView( model, options ) { var view = this; ScreenView.call( view, { layoutBounds: new Bounds2( 0, 0, 834, 504 ) } ); var modelPoint = new Vector2( 0, 0 ); // earth is 70px high in stage coordinates var viewPoint = new Vector2( this.layoutBounds.width / 2, this.layoutBounds.height - BackgroundNode.earthHeight ); var scale = 50; var modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( modelPoint, viewPoint, scale ); this.modelViewTransform = modelViewTransform; this.availableModelBoundsProperty = new Property(); this.availableModelBoundsProperty.linkAttribute( model, 'availableModelBounds' ); // The background this.backgroundNode = new BackgroundNode( this.layoutBounds ); this.addChild( this.backgroundNode ); this.gridNode = new GridNode( model.property( 'gridVisible' ), modelViewTransform ); this.addChild( this.gridNode ); var pieChartLegend = new PieChartLegend( model.skater, model.clearThermal.bind( model ), model.property( 'pieChartVisible' ), { tandem: options.tandem } ); this.addChild( pieChartLegend ); this.controlPanel = new EnergySkateParkBasicsControlPanel( model, { tandem: options.tandem, massSliderPhETIOID: options.massSliderPhETIOID } ); this.addChild( this.controlPanel ); this.controlPanel.right = this.layoutBounds.width - 5; this.controlPanel.top = 5; // For the playground screen, show attach/detach toggle buttons if ( model.draggableTracks ) { var property = model.draggableTracks ? new Property( true ) : new DerivedProperty( [ model.property( 'scene' ) ], function( scene ) { return scene === 2; } ); this.attachDetachToggleButtons = new AttachDetachToggleButtons( model.property( 'detachable' ), property, this.controlPanel.contentWidth, { top: this.controlPanel.bottom + 5, centerX: this.controlPanel.centerX } ); this.addChild( this.attachDetachToggleButtons ); } var containsAbove = function( bounds, x, y ) { return bounds.minX <= x && x <= bounds.maxX && y <= bounds.maxY; }; // Determine if the skater is onscreen or offscreen for purposes of highlighting the 'return skater' button. // Don't check whether the skater is underground since that is a rare case (only if the user is actively dragging a // control point near y=0 and the track curves below) and the skater will pop up again soon, see the related // flickering problem in #206 var onscreenProperty = new DerivedProperty( [ model.skater.positionProperty ], function( position ) { if ( !view.availableModelBounds ) { return true; } return view.availableModelBounds && containsAbove( view.availableModelBounds, position.x, position.y ); } ); var barGraphBackground = new BarGraphBackground( model.skater, model.property( 'barGraphVisible' ), model.clearThermal.bind( model ), { tandem: options.tandem } ); this.addChild( barGraphBackground ); if ( !model.draggableTracks ) { this.sceneSelectionPanel = new SceneSelectionPanel( model, this, modelViewTransform, { tandem: options.tandem } );// layout done in layout bounds this.addChild( this.sceneSelectionPanel ); } // Put the pie chart legend to the right of the bar chart, see #60, #192 pieChartLegend.mutate( { top: barGraphBackground.top, left: barGraphBackground.right + 8 } ); var playProperty = new Property( !model.property( 'paused' ).value ); model.property( 'paused' ).link( function( paused ) { playProperty.set( !paused ); } ); playProperty.link( function( playing ) { model.property( 'paused' ).set( !playing ); } ); var playPauseButton = new PlayPauseButton( playProperty, { phetioID: 'playPauseButton' } ).mutate( { scale: 0.6 } ); // Make the Play/Pause button bigger when it is showing the pause button, see #298 var pauseSizeIncreaseFactor = 1.35; playProperty.lazyLink( function( isPlaying ) { playPauseButton.scale( isPlaying ? ( 1 / pauseSizeIncreaseFactor ) : pauseSizeIncreaseFactor ); } ); var stepButton = new StepForwardButton( function() { model.manualStep(); }, playProperty, { phetioID: options.tandem.createTandem( 'stepButton' ) } ); // Make the step button the same size as the pause button. stepButton.mutate( { scale: playPauseButton.height / stepButton.height } ); model.property( 'paused' ).linkAttribute( stepButton, 'enabled' ); this.addChild( playPauseButton.mutate( { centerX: this.layoutBounds.centerX, bottom: this.layoutBounds.maxY - 15 } ) ); this.addChild( stepButton.mutate( { left: playPauseButton.right + 15, centerY: playPauseButton.centerY } ) ); this.resetAllButton = new ResetAllButton( { listener: model.reset.bind( model ), scale: 0.85, centerX: this.controlPanel.centerX, // Align vertically with other controls, see #134 centerY: (modelViewTransform.modelToViewY( 0 ) + this.layoutBounds.maxY) / 2 + 8, phetioID: options.tandem.createTandem( 'resetAllButton' ) } ); this.addChild( this.resetAllButton ); // The button to return the skater this.returnSkaterButton = new RectangularPushButton( { content: new Text( controlsRestartSkaterString, { maxWidth: 100 } ), listener: model.returnSkater.bind( model ), centerY: this.resetAllButton.centerY, // X updated in layoutBounds since the reset all button can move horizontally phetioID: options.tandem.createTandem( 'returnSkaterButton' ) } ); // Disable the return skater button when the skater is already at his initial coordinates model.skater.linkAttribute( 'moved', view.returnSkaterButton, 'enabled' ); this.addChild( this.returnSkaterButton ); this.addChild( new PlaybackSpeedControl( model.property( 'speed' ), { slowSpeedRadioButtonPhETIOID: options.tandem.createTandem( 'slowSpeedRadioButton' ), normalSpeedRadioButtonPhETIOID: options.tandem.createTandem( 'normalSpeedRadioButton' ) } ).mutate( { left: stepButton.right + 20, centerY: playPauseButton.centerY } ) ); var speedometerNode = new GaugeNode( // Hide the needle in for the background of the GaugeNode new Property( null ), propertiesSpeedString, { min: 0, max: 20 }, { // enable/disable updates based on whether the speedometer is visible updateEnabledProperty: model.property( 'speedometerVisible' ), pickable: false } ); model.property( 'speedometerVisible' ).linkAttribute( speedometerNode, 'visible' ); speedometerNode.centerX = this.layoutBounds.centerX; speedometerNode.top = this.layoutBounds.minY + 5; this.addChild( speedometerNode ); // Layer which will contain all of the tracks var trackLayer = new Node(); // Switch between selectable tracks if ( !model.draggableTracks ) { var trackNodes = model.tracks.map( function( track ) { return new TrackNode( model, track, modelViewTransform, view.availableModelBoundsProperty ); } ).getArray(); trackNodes.forEach( function( trackNode ) { trackLayer.addChild( trackNode ); } ); model.property( 'scene' ).link( function( scene ) { trackNodes[ 0 ].visible = (scene === 0); trackNodes[ 1 ].visible = (scene === 1); trackNodes[ 2 ].visible = (scene === 2); } ); } else { var addTrackNode = function( track ) { var trackNode = new TrackNode( model, track, modelViewTransform, view.availableModelBoundsProperty ); trackLayer.addChild( trackNode ); // When track removed, remove its view var itemRemovedListener = function( removed ) { if ( removed === track ) { trackLayer.removeChild( trackNode ); model.tracks.removeItemRemovedListener( itemRemovedListener );// Clean up memory leak } }; model.tracks.addItemRemovedListener( itemRemovedListener ); return trackNode; }; // Create the tracks for the track toolbox var interactiveTrackNodes = model.tracks.map( addTrackNode ).getArray(); // Add a panel behind the tracks var padding = 10; var trackCreationPanel = new Rectangle( (interactiveTrackNodes[ 0 ].left - padding / 2), (interactiveTrackNodes[ 0 ].top - padding / 2), (interactiveTrackNodes[ 0 ].width + padding), (interactiveTrackNodes[ 0 ].height + padding), 6, 6, { fill: 'white', stroke: 'black' } ); this.addChild( trackCreationPanel ); model.tracks.addItemAddedListener( addTrackNode ); var xTip = 20; var yTip = 8; var xControl = 12; var yControl = -5; var createArrowhead = function( angle, tail ) { var headWidth = 10; var headHeight = 10; var directionUnitVector = Vector2.createPolar( 1, angle ); var orthogonalUnitVector = directionUnitVector.perpendicular(); var tip = directionUnitVector.times( headHeight ).plus( tail ); return new Path( new Shape().moveToPoint( tail ).lineToPoint( tail.plus( orthogonalUnitVector.times( headWidth / 2 ) ) ).lineToPoint( tip ).lineToPoint( tail.plus( orthogonalUnitVector.times( -headWidth / 2 ) ) ).lineToPoint( tail ).close(), { fill: 'black' } ); }; var rightCurve = new Path( new Shape().moveTo( 0, 0 ).quadraticCurveTo( -xControl, yControl, -xTip, yTip ), { stroke: 'black', lineWidth: 3 } ); var arrowHead = createArrowhead( Math.PI - Math.PI / 3, new Vector2( -xTip, yTip ) ); var clearButtonEnabledProperty = model.property( 'clearButtonEnabled' ); clearButtonEnabledProperty.link( function( clearButtonEnabled ) { rightCurve.stroke = clearButtonEnabled ? 'black' : 'gray'; arrowHead.fill = clearButtonEnabled ? 'black' : 'gray'; } ); var clearButton = new EraserButton( { iconWidth: 30, baseColor: new Color( 221, 210, 32 ), phetioID: 'playgroundScreen.clearTracksButton' } ); clearButtonEnabledProperty.linkAttribute( clearButton, 'enabled' ); clearButton.addListener( function() {model.clearTracks();} ); this.addChild( clearButton.mutate( { left: 5, centerY: trackCreationPanel.centerY } ) ); } this.addChild( trackLayer ); // Check to see if WebGL was prevented by a query parameter var allowWebGL = phet.chipper.getQueryParameter( 'webgl' ) !== 'false'; // Use WebGL where available, but not on IE, due to https://github.com/phetsims/energy-skate-park-basics/issues/277 // and https://github.com/phetsims/scenery/issues/285 var webGLSupported = Util.isWebGLSupported && allowWebGL; var renderer = webGLSupported ? 'webgl' : null; var skaterNode = new SkaterNode( model.skater, this, modelViewTransform, model.getClosestTrackAndPositionAndParameter.bind( model ), model.getPhysicalTracks.bind( model ), renderer ); var gaugeNeedleNode = new GaugeNeedleNode( model.skater.property( 'speed' ), { min: 0, max: 20 }, { renderer: renderer } ); model.property( 'speedometerVisible' ).linkAttribute( gaugeNeedleNode, 'visible' ); gaugeNeedleNode.x = speedometerNode.x; gaugeNeedleNode.y = speedometerNode.y; this.addChild( gaugeNeedleNode ); this.addChild( new BarGraphForeground( model.skater, barGraphBackground, model.property( 'barGraphVisible' ), renderer ) ); this.addChild( skaterNode ); var pieChartNode = renderer === 'webgl' ? new PieChartWebGLNode( model.skater, model.property( 'pieChartVisible' ), modelViewTransform ) : new PieChartNode( model.skater, model.property( 'pieChartVisible' ), modelViewTransform ); this.addChild( pieChartNode ); // Buttons to return the skater when she is offscreen, see #219 var iconScale = 0.4; var returnSkaterToStartingPointButton = new RectangularPushButton( { content: new Image( skaterIconImage, { scale: iconScale } ), // green means "go" since the skater will likely start moving at this point baseColor: EnergySkateParkColorScheme.kineticEnergy, listener: model.returnSkater.bind( model ), phetioID: options.tandem.createTandem( 'returnSkaterToPreviousStartingPositionButton' ) } ); var returnSkaterToGroundButton = new RectangularPushButton( { content: new Image( skaterIconImage, { scale: iconScale } ), centerBottom: modelViewTransform.modelToViewPosition( model.skater.startingPosition ), baseColor: '#f4514e', // red for stop, since the skater will be stopped on the ground. listener: function() { model.skater.resetPosition(); }, phetioID: options.tandem.createTandem( 'returnSkaterToGroundButton' ) } ); this.addChild( returnSkaterToStartingPointButton ); this.addChild( returnSkaterToGroundButton ); // When the skater goes off screen, make the "return skater" button big onscreenProperty.link( function( skaterOnscreen ) { var buttonsVisible = !skaterOnscreen; returnSkaterToGroundButton.visible = buttonsVisible; returnSkaterToStartingPointButton.visible = buttonsVisible; if ( buttonsVisible ) { // Put the button where the skater will appear. Nudge it up a bit so the mouse can hit it from the drop site, // without being moved at all (to simplify repeat runs). var viewPosition = modelViewTransform.modelToViewPosition( model.skater.startingPosition ).plusXY( 0, 5 ); returnSkaterToStartingPointButton.centerBottom = viewPosition; // If the return skater button went offscreen, move it back on the screen, see #222 if ( returnSkaterToStartingPointButton.top < 5 ) { returnSkaterToStartingPointButton.top = 5; } } } ); // For debugging the visible bounds if ( showAvailableBounds ) { this.viewBoundsPath = new Path( null, { pickable: false, stroke: 'red', lineWidth: 10 } ); this.addChild( this.viewBoundsPath ); } }
function UnderPressureView( model ) { var self = this; ScreenView.call( this, { renderer: 'svg' } ); var mvt = ModelViewTransform2.createSinglePointScaleMapping( Vector2.ZERO, Vector2.ZERO, 70 ); //1m = 70px, (0,0) - top left corner //sky, earth and controls var backgroundNode = new BackgroundNode( model, mvt ); this.addChild( backgroundNode ); backgroundNode.moveToBack(); var scenes = {}; model.scenes.forEach( function( name ) { scenes[name] = new SceneView[name + 'PoolView']( model.sceneModels[name], mvt, self.layoutBounds ); scenes[name].visible = false; self.addChild( scenes[name] ); } ); //control panel this.controlPanel = new ControlPanel( model, 625, 5 ); this.addChild( this.controlPanel ); //control sliders this.fluidDensitySlider = new ControlSlider( model, model.fluidDensityProperty, model.units.getFluidDensityString, model.fluidDensityRange, { x: 585, y: 250, title: fluidDensityString, ticks: [ { title: WaterString, value: 1000 }, { title: GasolineString, value: model.fluidDensityRange.min }, { title: HoneyString, value: model.fluidDensityRange.max } ] } ); this.addChild( this.fluidDensitySlider ); this.gravitySlider = new ControlSlider( model, model.gravityProperty, model.units.getGravityString, model.gravityRange, { x: 585, y: 360, title: gravityString, decimals: 1, ticks: [ { title: EarthString, value: 9.8 }, { title: MarsString, value: model.gravityRange.min }, { title: JupiterString, value: model.gravityRange.max } ] } ); this.addChild( this.gravitySlider ); model.mysteryChoiceProperty.link( function( choice, oldChoice ) { if ( model.currentScene === 'Mystery' ) { self[choice + 'Slider'].disable(); if ( oldChoice ) { self[oldChoice + 'Slider'].enable(); } } } ); model.currentSceneProperty.link( function( currentScene ) { if ( currentScene === 'Mystery' ) { self[model.mysteryChoice + 'Slider'].disable(); } else { self.gravitySlider.enable(); self.fluidDensitySlider.enable(); } } ); // add reset button this.addChild( new ResetAllButton( { listener: function() { model.reset(); }, scale: 0.66, x: 745, y: model.height - 25 } ) ); this.barometersContainer = new Rectangle( 0, 0, 100, 130, 10, 10, {stroke: 'gray', lineWidth: 1, fill: '#f2fa6a', x: 520, y: 5} ); this.addChild( this.barometersContainer ); this.addChild( new SceneChoiceNode( model, 10, 260 ) ); //resize control panels var panels = [this.controlPanel, scenes.Mystery.mysteryPoolControls.choicePanel], maxWidth = 0; panels.forEach( function( panel ) { maxWidth = Math.max( maxWidth, panel.width / panel.transform.matrix.scaleVector.x ); } ); scenes.Mystery.mysteryPoolControls.choicePanel.resizeWidth( maxWidth ); panels.forEach( function( panel ) { panel.centerX = self.gravitySlider.centerX; } ); this.barometersContainer.x = this.controlPanel.x - 10 - this.barometersContainer.width; model.currentSceneProperty.link( function( value, oldValue ) { scenes[value].visible = true; if ( oldValue ) { scenes[oldValue].visible = false; } } ); this.addChild( new UnderPressureRuler( model, mvt, self.layoutBounds ) ); //barometers this.addChild( new BarometersContainer( model, mvt, this.barometersContainer.visibleBounds, self.layoutBounds ) ); }
/** * @param {MicroModel} model * @param {ModelViewTransform2} modelViewTransform * @constructor */ function MicroView( model, modelViewTransform ) { var thisView = this; ScreenView.call( thisView, PHScaleConstants.SCREEN_VIEW_OPTIONS ); // view-specific properties var viewProperties = new PropertySet( { ratioVisible: false, moleculeCountVisible: false, pHMeterExpanded: true, graphExpanded: true } ); // beaker var beakerNode = new BeakerNode( model.beaker, modelViewTransform ); var solutionNode = new SolutionNode( model.solution, model.beaker, modelViewTransform ); var volumeIndicatorNode = new VolumeIndicatorNode( model.solution.volumeProperty, model.beaker, modelViewTransform ); // dropper var DROPPER_SCALE = 0.85; var dropperNode = new DropperNode( model.dropper, modelViewTransform ); dropperNode.setScaleMagnitude( DROPPER_SCALE ); var dropperFluidNode = new DropperFluidNode( model.dropper, model.beaker, DROPPER_SCALE * dropperNode.getTipWidth(), modelViewTransform ); // faucets var waterFaucetNode = new WaterFaucetNode( model.waterFaucet, modelViewTransform ); var drainFaucetNode = new DrainFaucetNode( model.drainFaucet, modelViewTransform ); var SOLVENT_FLUID_HEIGHT = model.beaker.location.y - model.waterFaucet.location.y; var DRAIN_FLUID_HEIGHT = 1000; // tall enough that resizing the play area is unlikely to show bottom of fluid var waterFluidNode = new FaucetFluidNode( model.waterFaucet, new Property( Water.color ), SOLVENT_FLUID_HEIGHT, modelViewTransform ); var drainFluidNode = new FaucetFluidNode( model.drainFaucet, model.solution.colorProperty, DRAIN_FLUID_HEIGHT, modelViewTransform ); // 'H3O+/OH- ratio' representation var ratioNode = new RatioNode( model.beaker, model.solution, modelViewTransform, { visible: viewProperties.ratioVisibleProperty.get() } ); viewProperties.ratioVisibleProperty.linkAttribute( ratioNode, 'visible' ); // 'molecule count' representation var moleculeCountNode = new MoleculeCountNode( model.solution ); viewProperties.moleculeCountVisibleProperty.linkAttribute( moleculeCountNode, 'visible' ); // beaker controls var beakerControls = new BeakerControls( viewProperties.ratioVisibleProperty, viewProperties.moleculeCountVisibleProperty ); // graph var graphNode = new GraphNode( model.solution, viewProperties.graphExpandedProperty, { hasLinearFeature: true, logScaleHeight: 485, linearScaleHeight: 440 } ); // pH meter var pHMeterTop = 15; var pHMeterNode = new PHMeterNode( model.solution, modelViewTransform.modelToViewY( model.beaker.location.y ) - pHMeterTop, viewProperties.pHMeterExpandedProperty, { attachProbe: 'right' } ); // solutes combo box var soluteListParent = new Node(); var soluteComboBox = new SoluteComboBox( model.solutes, model.dropper.soluteProperty, soluteListParent ); var resetAllButton = new ResetAllButton( { scale: 1.32, listener: function() { model.reset(); viewProperties.reset(); graphNode.reset(); } } ); // Parent for all nodes added to this screen var rootNode = new Node( { children: [ // nodes are rendered in this order waterFluidNode, waterFaucetNode, drainFluidNode, drainFaucetNode, dropperFluidNode, dropperNode, solutionNode, pHMeterNode, ratioNode, beakerNode, moleculeCountNode, volumeIndicatorNode, beakerControls, graphNode, resetAllButton, soluteComboBox, soluteListParent // last, so that combo box list is on top ] } ); thisView.addChild( rootNode ); // Layout of nodes that don't have a location specified in the model moleculeCountNode.centerX = beakerNode.centerX; moleculeCountNode.bottom = beakerNode.bottom - 25; beakerControls.centerX = beakerNode.centerX; beakerControls.top = beakerNode.bottom + 10; pHMeterNode.left = modelViewTransform.modelToViewX( model.beaker.left ) - ( 0.4 * pHMeterNode.width ); pHMeterNode.top = pHMeterTop; graphNode.right = drainFaucetNode.left - 40; graphNode.top = pHMeterNode.top; soluteComboBox.left = pHMeterNode.right + 35; soluteComboBox.top = this.layoutBounds.top + pHMeterTop; resetAllButton.right = this.layoutBounds.right - 40; resetAllButton.bottom = this.layoutBounds.bottom - 20; }
/** * @param {Object[]} demos - each demo has these properties: * {string} label - label in the combo box * {function(layoutBounds:Bounds2): Node} createNode - creates the Node for the demo * {Node|null} node - the Node for the demo, created by DemosScreenView * @param {Object} [options] * @constructor */ function DemosScreenView( demos, options ) { options = _.extend( { selectedDemoLabel: null, // {string|null} label field of the demo to be selected initially // options related to the ComboBox that selects the demo comboBoxCornerRadius: 4, comboBoxLocation: new Vector2( 20, 20 ), // {Vector2} location of ComboBox used to select a demo comboBoxItemFont: new PhetFont( 20 ), // {Font} font used for ComboBox items comboBoxItemXMargin: 12, // {number} x margin around ComboBox items comboBoxItemYMargin: 8, // {number} y margin around ComboBox items // {boolean} see https://github.com/phetsims/sun/issues/386 // true = caches Nodes for all demos that have been selected // false = keeps only the Node for the selected demo in memory cacheDemos: false, tandem: Tandem.required }, options ); ScreenView.call( this, options ); var layoutBounds = this.layoutBounds; // Sort the demos by label, so that they appear in the combo box in alphabetical order demos = _.sortBy( demos, function( demo ) { return demo.label; } ); // All demos will be children of this node, to maintain rendering order with combo box list var demosParent = new Node(); this.addChild( demosParent ); // add each demo to the combo box var comboBoxItems = []; demos.forEach( function( demo ) { comboBoxItems.push( new ComboBoxItem( new Text( demo.label, { font: options.comboBoxItemFont } ), demo, { // demo.label is like ArrowNode or TimerNode, decorate it for use as a tandem. tandemName: 'demo' + demo.label + 'ComboBoxItem' } ) ); } ); // Parent for the combo box popup list var listParent = new Node(); this.addChild( listParent ); // Set the initial demo var selectedDemo = demos[ 0 ]; if ( options.selectedDemoLabel ) { selectedDemo = _.find( demos, function( demo ) { return ( demo.label === options.selectedDemoLabel ); } ); if ( !selectedDemo ) { throw new Error( 'demo not found: ' + options.selectedDemoLabel ); } } // Combo box for selecting which component to view var selectedDemoProperty = new Property( selectedDemo ); var comboBox = new ComboBox( comboBoxItems, selectedDemoProperty, listParent, { buttonFill: 'rgb( 218, 236, 255 )', cornerRadius: options.comboBoxCornerRadius, xMargin: options.comboBoxItemXMargin, yMargin: options.comboBoxItemYMargin, top: options.comboBoxLocation.x, left: options.comboBoxLocation.y, tandem: options.tandem.createTandem( 'comboBox' ) } ); this.addChild( comboBox ); // Make the selected demo visible selectedDemoProperty.link( function( demo, oldDemo ) { // clean up the previously selected demo if ( oldDemo ) { if ( options.cacheDemos ) { // make the old demo invisible oldDemo.node.visible = false; } else { // delete the old demo demosParent.removeChild( oldDemo.node ); oldDemo.node.dispose(); oldDemo.node = null; } } if ( demo.node ) { // If the selected demo has an associated node, make it visible. demo.node.visible = true; } else { // If the selected demo doesn't doesn't have an associated node, create it. demo.node = demo.createNode( layoutBounds, { tandem: options.tandem.createTandem( 'demo' + demo.label ) } ); demosParent.addChild( demo.node ); } } ); }
/** * @param {MakeIsotopesModel} makeIsotopesModel * @param {Tandem} tandem * @constructor */ function MakeIsotopesScreenView( makeIsotopesModel, tandem ) { // super type constructor ScreenView.call( this, { layoutBounds: ShredConstants.LAYOUT_BOUNDS } ); // Set up the model-canvas transform. IMPORTANT NOTES: The multiplier factors for the point in the view can be // adjusted to shift the center right or left, and the scale factor can be adjusted to zoom in or out (smaller // numbers zoom out, larger ones zoom in). this.modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( Vector2.ZERO, new Vector2( Util.roundSymmetric( this.layoutBounds.width * 0.4 ), Util.roundSymmetric( this.layoutBounds.height * 0.49 ) ), 1.0 ); // Layers upon which the various display elements are placed. This allows us to create the desired layering effects. var indicatorLayer = new Node(); this.addChild( indicatorLayer ); //adding this layer later so that its on the top var atomLayer = new Node(); // Create and add the Reset All Button in the bottom right, which resets the model var resetAllButton = new ResetAllButton( { listener: function() { makeIsotopesModel.reset(); scaleNode.reset(); symbolBox.expandedProperty.reset(); abundanceBox.expandedProperty.reset(); }, right: this.layoutBounds.maxX - 10, bottom: this.layoutBounds.maxY - 10 } ); resetAllButton.scale( 0.85 ); this.addChild( resetAllButton ); // Create the node that represents the scale upon which the atom sits. var scaleNode = new AtomScaleNode( makeIsotopesModel.particleAtom ); // The scale needs to sit just below the atom, and there are some "tweak factors" needed to get it looking right. scaleNode.setCenterBottom( new Vector2( this.modelViewTransform.modelToViewX( 0 ), this.bottom ) ); this.addChild( scaleNode ); // Create the node that contains both the atom and the neutron bucket. var bottomOfAtomPosition = new Vector2( scaleNode.centerX, scaleNode.top + 15 ); //empirically determined var atomAndBucketNode = new InteractiveIsotopeNode( makeIsotopesModel, this.modelViewTransform, bottomOfAtomPosition ); atomLayer.addChild( atomAndBucketNode ); // Add the interactive periodic table that allows the user to select the current element. Heaviest interactive // element is Neon for this sim. var periodicTableNode = new ExpandedPeriodicTableNode( makeIsotopesModel.numberAtom, 10, { tandem: tandem } ); periodicTableNode.scale( 0.65 ); periodicTableNode.top = 10; periodicTableNode.right = this.layoutBounds.width - 10; this.addChild( periodicTableNode ); // Add the legend/particle count indicator. var particleCountLegend = new ParticleCountDisplay( makeIsotopesModel.particleAtom, 13, 250 ); particleCountLegend.scale( 1.1 ); particleCountLegend.left = 20; particleCountLegend.top = periodicTableNode.visibleBounds.minY; indicatorLayer.addChild( particleCountLegend ); var symbolRectangle = new Rectangle( 0, 0, SYMBOL_BOX_WIDTH, SYMBOL_BOX_HEIGHT, 0, 0, { fill: 'white', stroke: 'black', lineWidth: 2 } ); // Add the symbol text. var symbolText = new Text( '', { font: new PhetFont( 150 ), fill: 'black', center: new Vector2( symbolRectangle.width / 2, symbolRectangle.height / 2 ) } ); // Add the listener to update the symbol text. var textCenter = new Vector2( symbolRectangle.width / 2, symbolRectangle.height / 2 ); // Doesn't need unlink as it stays through out the sim life makeIsotopesModel.particleAtom.protonCountProperty.link( function( protonCount ) { var symbol = AtomIdentifier.getSymbol( protonCount ); symbolText.text = protonCount > 0 ? symbol : ''; symbolText.center = textCenter; } ); symbolRectangle.addChild( symbolText ); // Add the proton count display. var protonCountDisplay = new Text( '0', { font: NUMBER_FONT, fill: 'red' } ); symbolRectangle.addChild( protonCountDisplay ); // Add the listener to update the proton count. // Doesn't need unlink as it stays through out the sim life makeIsotopesModel.particleAtom.protonCountProperty.link( function( protonCount ) { protonCountDisplay.text = protonCount; protonCountDisplay.left = NUMBER_INSET; protonCountDisplay.bottom = SYMBOL_BOX_HEIGHT - NUMBER_INSET; } ); // Add the mass number display. var massNumberDisplay = new Text( '0', { font: NUMBER_FONT, fill: 'black' } ); symbolRectangle.addChild( massNumberDisplay ); // Add the listener to update the mass number. // Doesn't need unlink as it stays through out the sim life makeIsotopesModel.particleAtom.massNumberProperty.link( function( massNumber ) { massNumberDisplay.text = massNumber; massNumberDisplay.left = NUMBER_INSET; massNumberDisplay.top = NUMBER_INSET; } ); symbolRectangle.scale( 0.20 ); var symbolBox = new AccordionBox( symbolRectangle, { cornerRadius: 3, titleNode: new Text( symbolString, { font: ShredConstants.ACCORDION_BOX_TITLE_FONT, maxWidth: ShredConstants.ACCORDION_BOX_TITLE_MAX_WIDTH } ), fill: ShredConstants.DISPLAY_PANEL_BACKGROUND_COLOR, expandedProperty: new Property( false ), minWidth: periodicTableNode.visibleBounds.width, contentAlign: 'center', titleAlignX: 'left', buttonAlign: 'right', expandCollapseButtonOptions: { touchAreaXDilation: OPEN_CLOSE_BUTTON_TOUCH_AREA_DILATION, touchAreaYDilation: OPEN_CLOSE_BUTTON_TOUCH_AREA_DILATION } } ); symbolBox.left = periodicTableNode.visibleBounds.minX; symbolBox.top = periodicTableNode.bottom + 10; this.addChild( symbolBox ); var abundanceBox = new AccordionBox( new TwoItemPieChartNode( makeIsotopesModel ), { cornerRadius: 3, titleNode: new Text( abundanceInNatureString, { font: ShredConstants.ACCORDION_BOX_TITLE_FONT, maxWidth: ShredConstants.ACCORDION_BOX_TITLE_MAX_WIDTH } ), fill: ShredConstants.DISPLAY_PANEL_BACKGROUND_COLOR, expandedProperty: new Property( false ), minWidth: periodicTableNode.visibleBounds.width, contentAlign: 'center', contentXMargin: 0, titleAlignX: 'left', buttonAlign: 'right', expandCollapseButtonOptions: { touchAreaXDilation: OPEN_CLOSE_BUTTON_TOUCH_AREA_DILATION, touchAreaYDilation: OPEN_CLOSE_BUTTON_TOUCH_AREA_DILATION } } ); abundanceBox.left = symbolBox.left; abundanceBox.top = symbolBox.bottom + 10; this.addChild( abundanceBox ); this.addChild( atomLayer ); }
/** * @constructor * * @param {PendulumLabModel} model * @param {ModelViewTransform2} modelViewTransform */ function PendulumLabScreenView( model, options ) { ScreenView.call( this ); // @private {PendulumLabModel} this.model = model; options = _.extend( { hasGravityTweakers: false, hasPeriodTimer: false }, options ); var modelViewTransform = PendulumLabConstants.MODEL_VIEW_TRANSFORM; var pendulaNode = new PendulaNode( model.pendula, modelViewTransform, { isAccelerationVisibleProperty: model.isAccelerationVisibleProperty, isVelocityVisibleProperty: model.isVelocityVisibleProperty } ); // create drag listener for the pendula var backgroundDragNode = new Plane(); var dragListener = new ClosestDragListener( 0.15, 0 ); // 15cm from mass is OK for touch pendulaNode.draggableItems.forEach( function( draggableItem ) { dragListener.addDraggableItem( draggableItem ); } ); backgroundDragNode.addInputListener( dragListener ); // @private {PeriodTraceNode} this.firstPeriodTraceNode = new PeriodTraceNode( model.pendula[ 0 ], modelViewTransform ); this.secondPeriodTraceNode = new PeriodTraceNode( model.pendula[ 1 ], modelViewTransform ); // create protractor node var protractorNode = new ProtractorNode( model.pendula, modelViewTransform ); // create a node to keep track of combo box var popupLayer = new Node(); var pendulumControlPanel = new PendulumControlPanel( model.pendula, model.numberOfPendulaProperty ); var globalControlPanel = new GlobalControlPanel( model, popupLayer, !!options.hasGravityTweakers ); // @protected this.rightPanelsContainer = new VBox( { spacing: PendulumLabConstants.PANEL_PADDING, children: [ pendulumControlPanel, globalControlPanel ], right: this.layoutBounds.right - PendulumLabConstants.PANEL_PADDING, top: this.layoutBounds.top + PendulumLabConstants.PANEL_PADDING } ); // create tools control panel (which controls the visibility of the ruler and stopwatch) var toolsControlPanelNode = new ToolsPanel( model.ruler.isVisibleProperty, model.stopwatch.isVisibleProperty, model.isPeriodTraceVisibleProperty, options.hasPeriodTimer, { maxWidth: 180, left: this.layoutBounds.left + PendulumLabConstants.PANEL_PADDING, bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING } ); // @protected {Node} this.toolsControlPanelNode = toolsControlPanelNode; // create pendulum system control panel (controls the length and mass of the pendula) var playbackControls = new PlaybackControlsNode( model.numberOfPendulaProperty, model.isPlayingProperty, model.timeSpeedProperty, model.stepManual.bind( model ), model.returnPendula.bind( model ), { x: this.layoutBounds.centerX, bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING } ); // create reset all button var resetAllButton = new ResetAllButton( { listener: model.reset.bind( model ), right: this.layoutBounds.right - PendulumLabConstants.PANEL_PADDING, bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING } ); // create ruler node var rulerNode = new PendulumLabRulerNode( model.ruler, modelViewTransform, this.layoutBounds ); rulerNode.left = this.layoutBounds.left + PendulumLabConstants.PANEL_PADDING; rulerNode.top = this.layoutBounds.top + PendulumLabConstants.PANEL_PADDING; model.ruler.setInitialLocationValue( rulerNode.center ); // @protected this.rulerNode = rulerNode; // create timer node var stopwatchNode = new StopwatchNode( model.stopwatch, this.layoutBounds ); stopwatchNode.bottom = rulerNode.bottom; stopwatchNode.right = Math.max( 167.75, toolsControlPanelNode.right ); // If we are only on this screen model.stopwatch.setInitialLocationValue( stopwatchNode.center ); // @protected this.stopwatchNode = stopwatchNode; // @protected this.arrowsPanelLayer = new Node(); this.energyGraphLayer = new Node(); this.periodTimerLayer = new Node(); var leftFloatingLayer = new Node( { children: [ this.energyGraphLayer, this.arrowsPanelLayer, toolsControlPanelNode ] } ); var rightFloatingLayer = new Node( { children: [ this.rightPanelsContainer, resetAllButton, popupLayer ] } ); // Layout for https://github.com/phetsims/pendulum-lab/issues/98 this.visibleBoundsProperty.lazyLink( function( visibleBounds ) { var dx = -visibleBounds.x; dx = Math.min( 200, dx ); leftFloatingLayer.x = -dx; rightFloatingLayer.x = dx; // set the drag bounds of the ruler and stopwatch rulerNode.movableDragHandler.setDragBounds( visibleBounds.erodedXY( rulerNode.width / 2, rulerNode.height / 2 ) ); stopwatchNode.movableDragHandler.setDragBounds( visibleBounds.erodedXY( stopwatchNode.width / 2, stopwatchNode.height / 2 ) ); } ); this.children = [ backgroundDragNode, protractorNode, leftFloatingLayer, rightFloatingLayer, playbackControls, this.firstPeriodTraceNode, this.secondPeriodTraceNode, pendulaNode, rulerNode, this.periodTimerLayer, stopwatchNode ]; }
/** * @param {CircuitConstructionKitModel} introScreenModel * @constructor */ function IntroScreenView( introScreenModel, tandem ) { var self = this; this.introScreenModel = introScreenModel; ScreenView.call( this ); this.sceneNodes = {}; var sceneSelectionRadioButtonGroup = new SceneSelectionRadioButtonGroup( introScreenModel.selectedSceneProperty ); // Reset All button var resetAllButton = new ResetAllButton( { listener: function() { introScreenModel.reset(); _.values( self.sceneNodes ).forEach( function( sceneNode ) { sceneNode.reset(); sceneNode.model.reset(); } ); } } ); this.addChild( resetAllButton ); introScreenModel.selectedSceneProperty.link( function( selectedScene ) { if ( !self.sceneNodes[ selectedScene ] ) { var options = { 0: { numberOfRightBatteriesInToolbox: 1, numberOfWiresInToolbox: 4, numberOfLightBulbsInToolbox: 0, numberOfResistorsInToolbox: 0, numberOfSwitchesInToolbox: 0 }, 1: { numberOfRightBatteriesInToolbox: 1, numberOfWiresInToolbox: 4, numberOfLightBulbsInToolbox: 0, numberOfResistorsInToolbox: 0, numberOfSwitchesInToolbox: 0 }, 2: { numberOfRightBatteriesInToolbox: 1, numberOfWiresInToolbox: 4, numberOfLightBulbsInToolbox: 0, numberOfResistorsInToolbox: 0, numberOfSwitchesInToolbox: 1 } }[ selectedScene ]; var sceneNode = new IntroSceneNode( new IntroSceneModel( self.layoutBounds, introScreenModel.selectedSceneProperty ), tandem.createTandem( selectedScene ), options ); sceneNode.visibleBoundsProperty.set( self.visibleBoundsProperty.value ); self.sceneNodes[ selectedScene ] = sceneNode; } // scene selection buttons go in back so that circuit elements may go in front self.children = [ resetAllButton, sceneSelectionRadioButtonGroup, self.sceneNodes[ selectedScene ] ]; } ); this.visibleBoundsProperty.link( function( visibleBounds ) { _.values( self.sceneNodes ).forEach( function( sceneNode ) { sceneNode.visibleBoundsProperty.set( visibleBounds ); } ); sceneSelectionRadioButtonGroup.mutate( { left: visibleBounds.left + LAYOUT_INSET, top: visibleBounds.top + LAYOUT_INSET } ); // Float the resetAllButton to the bottom right resetAllButton.mutate( { right: visibleBounds.right - LAYOUT_INSET, bottom: visibleBounds.bottom - LAYOUT_INSET } ); } ); }
/** * @param {FaradaysLawModel} model - Faraday's Law simulation model object * @param {Tandem} tandem * @constructor */ function FaradaysLawScreenView( model, tandem ) { ScreenView.call( this, { layoutBounds: FaradaysLawConstants.LAYOUT_BOUNDS, // a11y - TODO: Remove once https://github.com/phetsims/scenery-phet/issues/393 is complete addScreenSummaryNode: true } ); // screen Summary var summaryNode = new Node(); summaryNode.addChild( new Node( { tagName: 'p', innerContent: summaryDescriptionString } ) ); summaryNode.addChild( new Node( { tagName: 'p', innerContent: moveMagnetToPlayString } ) ); var playArea = new PlayAreaNode( { containerTagName: null } ); var circuitDescriptionNode = new CircuitDescriptionNode( model ); playArea.addChild( circuitDescriptionNode ); this.screenSummaryNode.addChild( summaryNode ); this.addChild( playArea ); // coils var bottomCoilNode = new CoilNode( CoilTypeEnum.FOUR_COIL, { x: model.bottomCoil.position.x, y: model.bottomCoil.position.y } ); var topCoilNode = new CoilNode( CoilTypeEnum.TWO_COIL, { x: model.topCoil.position.x, y: model.topCoil.position.y } ); // @public {Vector2[]} this.bottomCoilEndPositions = { topEnd: bottomCoilNode.endRelativePositions.topEnd.plus( model.bottomCoil.position ), bottomEnd: bottomCoilNode.endRelativePositions.bottomEnd.plus( model.bottomCoil.position ) }; // @public {Vector2[]} this.topCoilEndPositions = { topEnd: topCoilNode.endRelativePositions.topEnd.plus( model.topCoil.position ), bottomEnd: topCoilNode.endRelativePositions.bottomEnd.plus( model.topCoil.position ) }; // voltmeter and bulb created var voltmeterAndWiresNode = new VoltmeterAndWiresNode( model.voltmeter.needleAngleProperty, tandem.createTandem( 'voltmeterNode' ) ); var bulbNode = new BulbNode( model.voltageProperty, { center: FaradaysLawConstants.BULB_POSITION } ); // wires this.addChild( new CoilsWiresNode( this, model.topCoilVisibleProperty ) ); // exists for the lifetime of the sim, no need to dispose model.voltmeterVisibleProperty.link( function( showVoltmeter ) { voltmeterAndWiresNode.visible = showVoltmeter; } ); // When PhET-iO Studio makes the voltmeter invisible, we should also uncheck the checkbox. voltmeterAndWiresNode.on( 'visibility', function() { model.voltmeterVisibleProperty.value = voltmeterAndWiresNode.visible; } ); // bulb added this.addChild( bulbNode ); // coils added this.addChild( bottomCoilNode ); this.addChild( topCoilNode ); model.topCoilVisibleProperty.linkAttribute( topCoilNode, 'visible' ); // control panel var controlPanel = new ControlPanelNode( model, tandem ); this.addChild( controlPanel ); // voltmeter added this.addChild( voltmeterAndWiresNode ); // @private this.magnetNodeWithField = new MagnetNodeWithField( model, tandem.createTandem( 'magnetNode' ) ); this.addChild( this.magnetNodeWithField ); // a11y keyboard nav order this.accessibleOrder = [ this.screenSummaryNode, playArea, this.magnetNodeWithField, controlPanel ]; // move coils to front bottomCoilNode.frontImage.detach(); this.addChild( bottomCoilNode.frontImage ); bottomCoilNode.frontImage.center = model.bottomCoil.position.plus( new Vector2( CoilNode.xOffset, 0 ) ); topCoilNode.frontImage.detach(); this.addChild( topCoilNode.frontImage ); topCoilNode.frontImage.center = model.topCoil.position.plus( new Vector2( CoilNode.xOffset + CoilNode.twoOffset, 0 ) ); model.topCoilVisibleProperty.linkAttribute( topCoilNode.frontImage, 'visible' ); // const tcInnerBounds = Shape.bounds( this.magnetNodeWithField.regionManager._bottomCoilInnerBounds ).getStrokedShape(); // this.addChild( Rectangle.bounds( this.magnetNodeWithField.regionManager._topCoilInnerBounds, { stroke: 'red' } ) ) // this.addChild( Rectangle.bounds( this.magnetNodeWithField.regionManager._bottomCoilInnerBounds, { stroke: 'red' } ) ) }
/** * @param {MultipleCellsModel} model * @constructor */ function MultipleCellsScreenView( model ) { ScreenView.call( this ); var self = this; this.model = model; // Set up the model-canvas transform. The multiplier factors for the 2nd point can be adjusted to shift the center // right or left, and the scale factor can be adjusted to zoom in or out (smaller numbers zoom out, larger ones zoom // in). this.modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( Vector2.ZERO, new Vector2( this.layoutBounds.width * 0.455, this.layoutBounds.height * 0.56 ), 1E8 // "zoom factor" - smaller zooms out, larger zooms in ); // dialog constructed lazily because Dialog requires Sim bounds during construction var dialog = null; var buttonContent = new Text( showRealCellsString, { font: new PhetFont( 18 ), maxWidth: 140 } ); var showRealCellsButton = new RectangularPushButton( { content: buttonContent, touchAreaXDilation: 7, touchAreaYDilation: 7, baseColor: 'yellow', cornerRadius: GEEConstants.CORNER_RADIUS, listener: function() { if ( !dialog ) { dialog = new FluorescentCellsPictureDialog(); } dialog.show(); } } ); showRealCellsButton.left = this.layoutBounds.minX + 10; showRealCellsButton.top = this.layoutBounds.minY + 10; this.addChild( showRealCellsButton ); this.proteinLevelChartNode = new ProteinLevelChartNode( model.averageProteinLevelProperty ); this.addChild( this.proteinLevelChartNode ); this.proteinLevelChartNode.top = showRealCellsButton.top; this.proteinLevelChartNode.left = showRealCellsButton.right + 10; // Add the Reset All button. var resetAllButton = new ResetAllButton( { listener: function() { model.reset(); concentrationControlPanel.expandedProperty.reset(); affinityControlPanel.expandedProperty.reset(); degradationControlPanel.expandedProperty.reset(); self.proteinLevelChartNode.reset(); }, right: this.layoutBounds.maxX - 10, bottom: this.layoutBounds.maxY - 10 } ); this.addChild( resetAllButton ); // Add play/pause button. var playPauseButton = new PlayPauseButton( model.clockRunningProperty, { radius: 23, touchAreaDilation: 5 } ); this.addChild( playPauseButton ); var stepButton = new StepForwardButton( { isPlayingProperty: model.clockRunningProperty, listener: function() { model.stepInTime( 0.016 ); self.proteinLevelChartNode.addDataPoint( 0.016 ); }, radius: 15, touchAreaDilation: 5 } ); this.addChild( stepButton ); playPauseButton.bottom = resetAllButton.bottom; stepButton.centerY = playPauseButton.centerY; var cellLayer = new Node(); var invisibleCellLayer = new Node(); // for performance improvement load all cells at start of the sim this.addChild( cellLayer ); var cellNumberController = new ControllerNode( model.numberOfVisibleCellsProperty, 1, MultipleCellsModel.MaxCells, oneString, manyString ); var cellNumberControllerNode = new Node(); cellNumberControllerNode.addChild( cellNumberController ); var cellNumberLabel = new Text( cellsString, { font: new PhetFont( { size: 15, weight: 'bold' } ), maxWidth: 100 } ); cellNumberControllerNode.addChild( cellNumberLabel ); cellNumberLabel.centerX = cellNumberController.centerX; cellNumberLabel.bottom = cellNumberController.top - 5; var cellNumberControllerPanel = new Panel( cellNumberControllerNode, { cornerRadius: GEEConstants.CORNER_RADIUS, xMargin: 10, yMargin: 10, fill: new Color( 220, 236, 255 ) } ); this.addChild( cellNumberControllerPanel ); cellNumberControllerPanel.bottom = resetAllButton.bottom; cellNumberControllerPanel.centerX = this.proteinLevelChartNode.centerX; var cellNodes = []; for ( var i = 0; i < model.cellList.length; i++ ) { var cellNode = new ColorChangingCellNode( model.cellList[ i ], this.modelViewTransform ); cellNodes.push( cellNode ); invisibleCellLayer.addChild( cellNode ); } function addCellView( addedCellIndex ) { cellLayer.addChild( cellNodes[ addedCellIndex ] ); model.visibleCellList.addItemRemovedListener( function removalListener( removedCell ) { var removedCellIndex = model.cellList.indexOf( removedCell ); if ( removedCellIndex === addedCellIndex ) { cellLayer.removeChild( cellNodes[ addedCellIndex ] ); model.visibleCellList.removeItemRemovedListener( removalListener ); cellLayer.setScaleMagnitude( 1 ); var scaleFactor = Math.min( ( cellNumberControllerPanel.top - self.proteinLevelChartNode.bottom ) / cellLayer.height, 1 ); cellLayer.setScaleMagnitude( scaleFactor * 0.9 ); cellLayer.centerX = self.proteinLevelChartNode.centerX; cellLayer.centerY = self.proteinLevelChartNode.bottom + ( cellNumberControllerPanel.top - self.proteinLevelChartNode.bottom ) / 2; } } ); cellLayer.setScaleMagnitude( 1 ); var scaleFactor = Math.min( ( cellNumberControllerPanel.top - self.proteinLevelChartNode.bottom ) / cellLayer.height, 1 ); cellLayer.setScaleMagnitude( scaleFactor * 0.9 ); cellLayer.centerX = self.proteinLevelChartNode.centerX; cellLayer.centerY = self.proteinLevelChartNode.bottom + ( cellNumberControllerPanel.top - self.proteinLevelChartNode.bottom ) / 2; } // Set up an observer of the list of cells in the model so that the view representations can come and go as needed. model.visibleCellList.addItemAddedListener( function( addedCell ) { addCellView( model.cellList.indexOf( addedCell ) ); } ); model.visibleCellList.forEach( function( cell ) { addCellView( model.cellList.indexOf( cell ) ); } ); var concentrationControllers = [ { label: positiveTranscriptionFactorString, controlProperty: model.transcriptionFactorLevelProperty, minValue: CellProteinSynthesisSimulator.TranscriptionFactorCountRange.min, maxValue: CellProteinSynthesisSimulator.TranscriptionFactorCountRange.max, minLabel: lowString, maxLabel: highString, logScale: true }, { label: mRnaDestroyerString, controlProperty: model.mRnaDegradationRateProperty, minValue: CellProteinSynthesisSimulator.MRNADegradationRateRange.min, maxValue: CellProteinSynthesisSimulator.MRNADegradationRateRange.max, minLabel: lowString, maxLabel: highString, logScale: true } ]; var concentrationControlPanel = new ControlPanelNode( concentrationString, concentrationControllers ); var affinityControllers = [ { label: positiveTranscriptionFactorString, controlProperty: model.transcriptionFactorAssociationProbabilityProperty, minValue: CellProteinSynthesisSimulator.TFAssociationProbabilityRange.min, maxValue: CellProteinSynthesisSimulator.TFAssociationProbabilityRange.max, minLabel: lowString, maxLabel: highString, logScale: true }, { label: polymeraseString, controlProperty: model.polymeraseAssociationProbabilityProperty, minValue: CellProteinSynthesisSimulator.PolymeraseAssociationProbabilityRange.min, maxValue: CellProteinSynthesisSimulator.PolymeraseAssociationProbabilityRange.max, minLabel: lowString, maxLabel: highString, logScale: false } ]; var affinityControlPanel = new ControlPanelNode( affinitiesString, affinityControllers ); var degradationControllers = [ { label: proteinString, controlProperty: model.proteinDegradationRateProperty, minValue: CellProteinSynthesisSimulator.ProteinDegradationRange.min, maxValue: CellProteinSynthesisSimulator.ProteinDegradationRange.max, minLabel: slowString, maxLabel: fastString, logScale: false } ]; var degradationControlPanel = new ControlPanelNode( degradationString, degradationControllers ); this.addChild( concentrationControlPanel ); this.addChild( affinityControlPanel ); this.addChild( degradationControlPanel ); concentrationControlPanel.right = this.layoutBounds.maxX - 10; concentrationControlPanel.top = this.layoutBounds.minY + 10; affinityControlPanel.right = concentrationControlPanel.right; affinityControlPanel.top = concentrationControlPanel.bottom + 10; degradationControlPanel.right = affinityControlPanel.right; degradationControlPanel.top = affinityControlPanel.bottom + 10; playPauseButton.bottom = resetAllButton.bottom; stepButton.centerY = playPauseButton.centerY; stepButton.right = degradationControlPanel.left - 20; playPauseButton.right = stepButton.left - 10; }