define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var Dimension2 = require( 'DOT/Dimension2' ); var inherit = require( 'PHET_CORE/inherit' ); var LaserPointerNode = require( 'SCENERY_PHET/LaserPointerNode' ); /** * @param {Light} light * @param {ModelViewTransform2} modelViewTransform * @param {Tandem} tandem * @constructor */ function LightNode( light, modelViewTransform, tandem ) { LaserPointerNode.call( this, light.onProperty, { bodySize: new Dimension2( 126, 78 ), nozzleSize: new Dimension2( 16, 65 ), buttonRadius: 26, buttonTouchAreaDilation: 25, translation: modelViewTransform.modelToViewPosition( light.location ), tandem: tandem } ); } beersLawLab.register( 'LightNode', LightNode ); return inherit( LaserPointerNode, LightNode ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var inherit = require( 'PHET_CORE/inherit' ); var PrecipitateParticleIO = require( 'BEERS_LAW_LAB/concentration/model/PrecipitateParticleIO' ); var SoluteParticle = require( 'BEERS_LAW_LAB/concentration/model/SoluteParticle' ); /** * @param {Solute} solute * @param {Vector2} location location in the beaker's coordinate frame * @param {number} orientation in radians * @param {Object} [options] * @constructor */ function PrecipitateParticle( solute, location, orientation, options ) { options = _.extend( { phetioType: PrecipitateParticleIO }, options ); SoluteParticle.call( this, solute.particleColor, solute.particleSize, location, orientation, options ); // @public (phet-io) this.solute = solute; } beersLawLab.register( 'PrecipitateParticle', PrecipitateParticle ); return inherit( SoluteParticle, PrecipitateParticle ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var ChemUtils = require( 'NITROGLYCERIN/ChemUtils' ); // strings var drinkMixString = require( 'string!BEERS_LAW_LAB/drinkMix' ); var BLLSymbols = { COBALT_II_NITRATE: ChemUtils.toSubscript( 'Co(NO3)2' ), COBALT_CHLORIDE: ChemUtils.toSubscript( 'CoCl2' ), COPPER_SULFATE: ChemUtils.toSubscript( 'CuSO4' ), DRINK_MIX: drinkMixString, NICKEL_II_CHLORIDE: ChemUtils.toSubscript( 'NiCl2' ), POTASSIUM_CHROMATE: ChemUtils.toSubscript( 'K2CrO4' ), POTASSIUM_DICHROMATE: ChemUtils.toSubscript( 'K2Cr2O7' ), POTASSIUM_PERMANGANATE: ChemUtils.toSubscript( 'KMnO4' ), SODIUM_CHLORIDE: ChemUtils.toSubscript( 'NaCl' ), WATER: ChemUtils.toSubscript( 'H2O' ) }; beersLawLab.register( 'BLLSymbols', BLLSymbols ); return BLLSymbols; } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var ConcentrationSolution = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationSolution' ); var inherit = require( 'PHET_CORE/inherit' ); var Rectangle = require( 'SCENERY/nodes/Rectangle' ); /** * @param {Solvent} solvent * @param {Property.<Solute>} soluteProperty * @param {Dropper} dropper * @param {Beaker} beaker * @param {number} tipWidth * @param {ModelViewTransform2} modelViewTransform * @constructor */ function StockSolutionNode( solvent, soluteProperty, dropper, beaker, tipWidth, modelViewTransform ) { var self = this; Rectangle.call( this, 0, 0, 0, 0, { lineWidth: 1 } ); // shape and location var updateShapeAndLocation = function() { // path if ( dropper.dispensingProperty.get() && !dropper.emptyProperty.get() ) { self.setRect( -tipWidth / 2, 0, tipWidth, beaker.location.y - dropper.locationProperty.get().y ); } else { self.setRect( 0, 0, 0, 0 ); } // move this node to the dropper's location self.translation = modelViewTransform.modelToViewPosition( dropper.locationProperty.get() ); }; dropper.locationProperty.link( updateShapeAndLocation ); dropper.dispensingProperty.link( updateShapeAndLocation ); dropper.emptyProperty.link( updateShapeAndLocation ); // set color to match solute soluteProperty.link( function( solute ) { var color = ConcentrationSolution.createColor( solvent, solute, solute.stockSolutionConcentration ); self.fill = color; self.stroke = color.darkerColor(); } ); // hide this node when the dropper is invisible dropper.visibleProperty.link( function( visible ) { self.setVisible( visible ); } ); } beersLawLab.register( 'StockSolutionNode', StockSolutionNode ); return inherit( Rectangle, StockSolutionNode ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var BooleanProperty = require( 'AXON/BooleanProperty' ); var inherit = require( 'PHET_CORE/inherit' ); var NumberProperty = require( 'AXON/NumberProperty' ); /** * @param {number} maxEvaporationRate L/sec * @param {ConcentrationSolution} solution * @param {Tandem} tandem * @constructor */ function Evaporator( maxEvaporationRate, solution, tandem ) { var self = this; this.maxEvaporationRate = maxEvaporationRate; // @public (read-only) L/sec // @public this.evaporationRateProperty = new NumberProperty( 0, { tandem: tandem.createTandem( 'evaporationRateProperty' ), units: 'liters/second' } ); // L/sec this.enabledProperty = new BooleanProperty( true, { tandem: tandem.createTandem( 'enabledProperty' ) } ); // disable when the volume gets to zero solution.volumeProperty.link( function( volume ) { self.enabledProperty.set( volume > 0 ); } ); // when disabled, set the rate to zero this.enabledProperty.link( function( enabled ) { if ( !enabled ) { self.evaporationRateProperty.set( 0 ); } } ); } beersLawLab.register( 'Evaporator', Evaporator ); return inherit( Object, Evaporator, { // @public reset: function() { this.evaporationRateProperty.reset(); this.enabledProperty.reset(); } } ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var ObjectIO = require( 'TANDEM/types/ObjectIO' ); var phetioInherit = require( 'TANDEM/phetioInherit' ); var validate = require( 'AXON/validate' ); // ifphetio var phetioEngine = require( 'ifphetio!PHET_IO/phetioEngine' ); /** * @param {BeersLawSolution} beersLawSolution * @param {string} phetioID * @constructor */ function BeersLawSolutionIO( beersLawSolution, phetioID ) { ObjectIO.call( this, beersLawSolution, phetioID ); } phetioInherit( ObjectIO, 'BeersLawSolutionIO', BeersLawSolutionIO, {}, { documentation: 'The solution for the sim', validator: { isValidValue: v => v instanceof phet.beersLawLab.BeersLawSolution }, /** * Serializes an instance. * @param {BeersLawSolution} beersLawSolution * @returns {Object} * @override */ toStateObject: function( beersLawSolution ) { validate( beersLawSolution, this.validator ); return beersLawSolution.tandem.phetioID; }, /** * Deserializes an instance. * Because the simulation has a Property that contains BeersLawSolution, * the Property relies on these methods for saving and loading the values. * @param {Object} stateObject * @returns {BeersLawSolution} * @override */ fromStateObject: function( stateObject ) { return phetioEngine.getPhetioObject( stateObject ); } } ); beersLawLab.register( 'BeersLawSolutionIO', BeersLawSolutionIO ); return BeersLawSolutionIO; } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var Color = require( 'SCENERY/util/Color' ); var inherit = require( 'PHET_CORE/inherit' ); /** * @param {number} minConcentration - mol/L * @param {Color} minColor * @param {number} midConcentration - mol/L * @param {Color} midColor * @param {number} maxConcentration - mol/L (saturation point) * @param {Color} maxColor * @constructor */ function SoluteColorScheme( minConcentration, minColor, midConcentration, midColor, maxConcentration, maxColor ) { this.minColor = minColor; this.midColor = midColor; this.maxColor = maxColor; this.minConcentration = minConcentration; this.midConcentration = midConcentration; this.maxConcentration = maxConcentration; } beersLawLab.register( 'SoluteColorScheme', SoluteColorScheme ); return inherit( Object, SoluteColorScheme, { /** * Converts a concentration value to a Color, using a linear interpolation of RGB colors. * @param {number} concentration - mol/L * @returns {Color} color */ concentrationToColor: function( concentration ) { if ( concentration >= this.maxConcentration ) { return this.maxColor; } else if ( concentration <= this.minConcentration ) { return this.minColor; } else if ( concentration <= this.midConcentration ) { return Color.interpolateRGBA( this.minColor, this.midColor, ( concentration - this.minConcentration ) / ( this.midConcentration - this.minConcentration ) ); } else { return Color.interpolateRGBA( this.midColor, this.maxColor, ( concentration - this.midConcentration ) / ( this.maxConcentration - this.midConcentration ) ); } } } ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var BeersLawModel = require( 'BEERS_LAW_LAB/beerslaw/model/BeersLawModel' ); var BeersLawScreenView = require( 'BEERS_LAW_LAB/beerslaw/view/BeersLawScreenView' ); var Image = require( 'SCENERY/nodes/Image' ); var inherit = require( 'PHET_CORE/inherit' ); var ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' ); var Screen = require( 'JOIST/Screen' ); var Vector2 = require( 'DOT/Vector2' ); // strings var screenBeersLawString = require( 'string!BEERS_LAW_LAB/screen.beersLaw' ); // image var screenIcon = require( 'image!BEERS_LAW_LAB/BeersLaw-screen-icon.jpg' ); /** * @param {Tandem} tandem * @constructor */ function BeersLawScreen( tandem ) { // No offset, scale 125x when going from model to view (1cm == 125 pixels) var modelViewTransform = ModelViewTransform2.createOffsetScaleMapping( new Vector2( 0, 0 ), 125 ); var options = { name: screenBeersLawString, homeScreenIcon: new Image( screenIcon ), tandem: tandem }; Screen.call( this, function() { return new BeersLawModel( modelViewTransform, tandem.createTandem( 'model' ) ); }, function( model ) { return new BeersLawScreenView( model, modelViewTransform, tandem.createTandem( 'view' ) ); }, options ); } beersLawLab.register( 'BeersLawScreen', BeersLawScreen ); return inherit( Screen, BeersLawScreen ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var ConcentrationModel = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationModel' ); var ConcentrationScreenView = require( 'BEERS_LAW_LAB/concentration/view/ConcentrationScreenView' ); var Image = require( 'SCENERY/nodes/Image' ); var inherit = require( 'PHET_CORE/inherit' ); var ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' ); var Screen = require( 'JOIST/Screen' ); // strings var screenConcentrationString = require( 'string!BEERS_LAW_LAB/screen.concentration' ); // images var screenIcon = require( 'image!BEERS_LAW_LAB/Concentration-screen-icon.jpg' ); /** * @param {Tandem} tandem * @constructor */ function ConcentrationScreen( tandem ) { var modelViewTransform = ModelViewTransform2.createIdentity(); var options = { name: screenConcentrationString, homeScreenIcon: new Image( screenIcon ), tandem: tandem }; Screen.call( this, function() { return new ConcentrationModel( { tandem: tandem.createTandem( 'model' ) } ); }, function( model ) { return new ConcentrationScreenView( model, modelViewTransform, tandem.createTandem( 'view' ) ); }, options ); } beersLawLab.register( 'ConcentrationScreen', ConcentrationScreen ); return inherit( Screen, ConcentrationScreen ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var inherit = require( 'PHET_CORE/inherit' ); var PhetFont = require( 'SCENERY_PHET/PhetFont' ); var StringUtils = require( 'PHETCOMMON/util/StringUtils' ); var Text = require( 'SCENERY/nodes/Text' ); var Util = require( 'DOT/Util' ); // strings var pattern0SoluteAmountString = require( 'string!BEERS_LAW_LAB/pattern.0soluteAmount' ); // constants var DECIMAL_PLACES = 0; /** * @param {Property.<number>} soluteGramsProperty - grams of solute * @param {Object} [options] * @constructor */ function SoluteGramsNode( soluteGramsProperty, options ) { options = _.extend( { font: new PhetFont( 22 ) }, options ); var self = this; Text.call( this, '', options ); soluteGramsProperty.link( function( soluteGrams ) { self.text = StringUtils.format( pattern0SoluteAmountString, Util.toFixed( soluteGrams, DECIMAL_PLACES ) ); } ); } beersLawLab.register( 'SoluteGramsNode', SoluteGramsNode ); return inherit( Text, SoluteGramsNode ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var ConcentrationControl = require( 'BEERS_LAW_LAB/beerslaw/view/ConcentrationControl' ); var inherit = require( 'PHET_CORE/inherit' ); var Panel = require( 'SUN/Panel' ); var SolutionComboBox = require( 'BEERS_LAW_LAB/beerslaw/view/SolutionComboBox' ); var ToggleNode = require( 'SUN/ToggleNode' ); var VBox = require( 'SCENERY/nodes/VBox' ); /** * @param {BeersLawSolution[]} solutions * @param {Property.<BeersLawSolution>} currentSolutionProperty * @param {Node} solutionListParent * @param {Tandem} tandem * @param {Object} [options] * @constructor */ function SolutionControls( solutions, currentSolutionProperty, solutionListParent, tandem, options ) { options = _.extend( { xMargin: 15, yMargin: 15, fill: '#F0F0F0', stroke: 'gray', lineWidth: 1, tandem: tandem }, options ); // combo box, to select a solution var comboBox = new SolutionComboBox( solutions, currentSolutionProperty, solutionListParent, tandem.createTandem( 'comboBox' ) ); // {{value:{BeersLawSolution}, node:{ConcentrationControl}} - concentration controls, one for each solution var toggleNodeElements = solutions.map( function( solution ) { return { value: solution, node: new ConcentrationControl( solution, { visible: false, tandem: tandem.createTandem( solution.internalName + 'ConcentrationControl' ), phetioDocumentation: 'the concentration control for ' + solution.name } ) }; } ); // Makes the control visible for the selected solution var toggleNode = new ToggleNode( currentSolutionProperty, toggleNodeElements ); var contentNode = new VBox( { spacing: 15, align: 'left', children: [ comboBox, toggleNode ] } ); Panel.call( this, contentNode, options ); } beersLawLab.register( 'SolutionControls', SolutionControls ); return inherit( Panel, SolutionControls ); } );
define( function( require ) { 'use strict'; // modules var Beaker = require( 'BEERS_LAW_LAB/concentration/model/Beaker' ); var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var BLLConstants = require( 'BEERS_LAW_LAB/common/BLLConstants' ); var Bounds2 = require( 'DOT/Bounds2' ); var ConcentrationMeter = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationMeter' ); var ConcentrationModelIO = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationModelIO' ); var ConcentrationSolution = require( 'BEERS_LAW_LAB/concentration/model/ConcentrationSolution' ); var Dimension2 = require( 'DOT/Dimension2' ); var Dropper = require( 'BEERS_LAW_LAB/concentration/model/Dropper' ); var Evaporator = require( 'BEERS_LAW_LAB/concentration/model/Evaporator' ); var Faucet = require( 'BEERS_LAW_LAB/concentration/model/Faucet' ); var inherit = require( 'PHET_CORE/inherit' ); var PhetioObject = require( 'TANDEM/PhetioObject' ); var Precipitate = require( 'BEERS_LAW_LAB/concentration/model/Precipitate' ); var Property = require( 'AXON/Property' ); var PropertyIO = require( 'AXON/PropertyIO' ); var Shaker = require( 'BEERS_LAW_LAB/concentration/model/Shaker' ); var ShakerParticles = require( 'BEERS_LAW_LAB/concentration/model/ShakerParticles' ); var Solute = require( 'BEERS_LAW_LAB/concentration/model/Solute' ); var SoluteIO = require( 'BEERS_LAW_LAB/concentration/model/SoluteIO' ); var StringProperty = require( 'AXON/StringProperty' ); var Tandem = require( 'TANDEM/Tandem' ); var Vector2 = require( 'DOT/Vector2' ); // constants var SOLUTION_VOLUME_RANGE = BLLConstants.SOLUTION_VOLUME_RANGE; // L var SOLUTE_AMOUNT_RANGE = BLLConstants.SOLUTE_AMOUNT_RANGE; // moles var MAX_EVAPORATION_RATE = 0.25; // L/sec var MAX_FAUCET_FLOW_RATE = 0.25; // L/sec var DROPPER_FLOW_RATE = 0.05; // L/sec var SHAKER_MAX_DISPENSING_RATE = 0.2; // mol/sec /** * @param {Object} [options] * @constructor */ function ConcentrationModel( options ) { var self = this; options = _.extend( { tandem: Tandem.required, phetioType: ConcentrationModelIO, phetioState: false // does not contribute self-state, all of the state is from child instances (via composition) }, options ); var tandem = options.tandem; // @public Solutes, in rainbow (ROYGBIV) order. this.solutes = [ Solute.DRINK_MIX, Solute.COBALT_II_NITRATE, Solute.COBALT_CHLORIDE, Solute.POTASSIUM_DICHROMATE, Solute.POTASSIUM_CHROMATE, Solute.NICKEL_II_CHLORIDE, Solute.COPPER_SULFATE, Solute.POTASSIUM_PERMANGANATE, Solute.SODIUM_CHLORIDE ]; // @public this.soluteProperty = new Property( this.solutes[ 0 ], { tandem: tandem.createTandem( 'soluteProperty' ), phetioType: PropertyIO( SoluteIO ) } ); this.soluteFormProperty = new StringProperty( 'solid', { validValues: [ 'solid', 'solution' ], tandem: tandem.createTandem( 'soluteFormProperty' ) } ); // @public this.solution = new ConcentrationSolution( this.soluteProperty, SOLUTE_AMOUNT_RANGE.defaultValue, SOLUTION_VOLUME_RANGE.defaultValue, tandem.createTandem( 'solution' ) ); this.beaker = new Beaker( new Vector2( 350, 550 ), new Dimension2( 600, 300 ), 1 ); this.precipitate = new Precipitate( this.solution, this.beaker, { tandem: tandem.createTandem( 'precipitate' ) } ); this.shaker = new Shaker( new Vector2( this.beaker.location.x, 170 ), new Bounds2( 250, 50, 575, 210 ), 0.75 * Math.PI, this.soluteProperty, SHAKER_MAX_DISPENSING_RATE, this.soluteFormProperty.get() === 'solid', { tandem: tandem.createTandem( 'shaker' ) } ); this.shakerParticles = new ShakerParticles( this.shaker, this.solution, this.beaker, { tandem: tandem.createTandem( 'shakerParticles' ) } ); this.dropper = new Dropper( new Vector2( this.beaker.location.x, 225 ), new Bounds2( 260, 225, 580, 225 ), this.soluteProperty, DROPPER_FLOW_RATE, this.soluteFormProperty.get() === 'solution', { tandem: tandem.createTandem( 'dropper' ) } ); this.evaporator = new Evaporator( MAX_EVAPORATION_RATE, this.solution, tandem.createTandem( 'evaporator' ) ); this.solventFaucet = new Faucet( new Vector2( 155, 220 ), -400, 45, MAX_FAUCET_FLOW_RATE, tandem.createTandem( 'solventFaucet' ) ); this.drainFaucet = new Faucet( new Vector2( 750, 630 ), this.beaker.getRight(), 45, MAX_FAUCET_FLOW_RATE, tandem.createTandem( 'drainFaucet' ) ); this.concentrationMeter = new ConcentrationMeter( new Vector2( 785, 210 ), new Bounds2( 10, 150, 835, 680 ), new Vector2( 750, 370 ), new Bounds2( 30, 150, 966, 680 ), { tandem: tandem.createTandem( 'concentrationMeter' ) } ); // When the solute is changed, the amount of solute resets to 0. This is a lazyLink instead of link so that // the simulation can be launched with a nonzero solute amount (using PhET-iO) this.soluteProperty.lazyLink( function() { self.solution.soluteAmountProperty.set( 0 ); } ); // Enable faucets and dropper based on amount of solution in the beaker. this.solution.volumeProperty.link( function( volume ) { self.solventFaucet.enabledProperty.set( volume < SOLUTION_VOLUME_RANGE.max ); self.drainFaucet.enabledProperty.set( volume > SOLUTION_VOLUME_RANGE.min ); self.dropper.enabledProperty.set( !self.dropper.emptyProperty.get() && ( volume < SOLUTION_VOLUME_RANGE.max ) ); } ); // Empty shaker and dropper when max solute is reached. this.solution.soluteAmountProperty.link( function( soluteAmount ) { var containsMaxSolute = ( soluteAmount >= SOLUTE_AMOUNT_RANGE.max ); self.shaker.emptyProperty.set( containsMaxSolute ); self.dropper.emptyProperty.set( containsMaxSolute ); self.dropper.enabledProperty.set( !self.dropper.emptyProperty.get() && !containsMaxSolute && self.solution.volumeProperty.get() < SOLUTION_VOLUME_RANGE.max ); } ); PhetioObject.call( this, options ); } beersLawLab.register( 'ConcentrationModel', ConcentrationModel ); return inherit( PhetioObject, ConcentrationModel, { /* * May be called from PhET-iO before the UI is constructed to choose a different set of solutes. The first solute * becomes the selected solute * @param {Array.<Solute>} solutes * @public */ setSolutes: function( solutes ) { assert && assert( solutes.length > 0, 'Must specify at least one solute' ); this.solutes = solutes; this.soluteProperty.value = solutes[ 0 ]; }, // @public Resets all model elements reset: function() { this.soluteProperty.reset(); this.soluteFormProperty.reset(); this.solution.reset(); this.shaker.reset(); this.shakerParticles.reset(); this.dropper.reset(); this.evaporator.reset(); this.solventFaucet.reset(); this.drainFaucet.reset(); this.concentrationMeter.reset(); }, /* * Moves time forward by the specified amount. * @param deltaSeconds clock time change, in seconds. * @public */ step: function( deltaSeconds ) { this.addSolventFromInputFaucet( deltaSeconds ); this.drainSolutionFromOutputFaucet( deltaSeconds ); this.addStockSolutionFromDropper( deltaSeconds ); this.evaporateSolvent( deltaSeconds ); this.propagateShakerParticles( deltaSeconds ); this.createShakerParticles(); }, // @private Add solvent from the input faucet addSolventFromInputFaucet: function( deltaSeconds ) { this.addSolvent( this.solventFaucet.flowRateProperty.get() * deltaSeconds ); }, // @private Drain solution from the output faucet drainSolutionFromOutputFaucet: function( deltaSeconds ) { var drainVolume = this.drainFaucet.flowRateProperty.get() * deltaSeconds; if ( drainVolume > 0 ) { var concentration = this.solution.concentrationProperty.get(); // get concentration before changing volume var volumeRemoved = this.removeSolvent( drainVolume ); this.removeSolute( concentration * volumeRemoved ); } }, // @private Add stock solution from dropper addStockSolutionFromDropper: function( deltaSeconds ) { var dropperVolume = this.dropper.flowRateProperty.get() * deltaSeconds; if ( dropperVolume > 0 ) { // defer update of precipitateAmount until we've changed both volume and solute amount, see concentration#1 this.solution.updatePrecipitateAmount = false; var volumeAdded = this.addSolvent( dropperVolume ); this.solution.updatePrecipitateAmount = true; this.addSolute( this.solution.soluteProperty.get().stockSolutionConcentration * volumeAdded ); } }, // @private Evaporate solvent evaporateSolvent: function( deltaSeconds ) { this.removeSolvent( this.evaporator.evaporationRateProperty.get() * deltaSeconds ); }, // @private Propagates solid solute that came out of the shaker propagateShakerParticles: function( deltaSeconds ) { this.shakerParticles.step( deltaSeconds ); }, // @private Creates new solute particles when the shaker is shaken. createShakerParticles: function() { this.shaker.step(); }, // @private Adds solvent to the solution. Returns the amount actually added. addSolvent: function( deltaVolume ) { if ( deltaVolume > 0 ) { var volumeProperty = this.solution.volumeProperty; var volumeBefore = volumeProperty.get(); volumeProperty.set( Math.min( SOLUTION_VOLUME_RANGE.max, volumeProperty.get() + deltaVolume ) ); return volumeProperty.get() - volumeBefore; } else { return 0; } }, // @private Removes solvent from the solution. Returns the amount actually removed. removeSolvent: function( deltaVolume ) { if ( deltaVolume > 0 ) { var volumeProperty = this.solution.volumeProperty; var volumeBefore = volumeProperty.get(); volumeProperty.set( Math.max( SOLUTION_VOLUME_RANGE.min, volumeProperty.get() - deltaVolume ) ); return volumeBefore - volumeProperty.get(); } else { return 0; } }, // @private Adds solute to the solution. Returns the amount actually added. addSolute: function( deltaAmount ) { if ( deltaAmount > 0 ) { var amountBefore = this.solution.soluteAmountProperty.get(); this.solution.soluteAmountProperty.set( Math.min( SOLUTE_AMOUNT_RANGE.max, this.solution.soluteAmountProperty.get() + deltaAmount ) ); return this.solution.soluteAmountProperty.get() - amountBefore; } else { return 0; } }, // @private Removes solute from the solution. Returns the amount actually removed. removeSolute: function( deltaAmount ) { if ( deltaAmount > 0 ) { var amountBefore = this.solution.soluteAmountProperty.get(); this.solution.soluteAmountProperty.set( Math.max( SOLUTE_AMOUNT_RANGE.min, this.solution.soluteAmountProperty.get() - deltaAmount ) ); return amountBefore - this.solution.soluteAmountProperty.get(); } else { return 0; } } }, { SOLUTION_VOLUME_RANGE: SOLUTION_VOLUME_RANGE // Exported for access to phet-io API } ); } );
define( function( require ) { 'use strict'; // modules var ATDetectorNode = require( 'BEERS_LAW_LAB/beerslaw/view/ATDetectorNode' ); var BeamNode = require( 'BEERS_LAW_LAB/beerslaw/view/BeamNode' ); var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var BLLConstants = require( 'BEERS_LAW_LAB/common/BLLConstants' ); var BLLQueryParameters = require( 'BEERS_LAW_LAB/common/BLLQueryParameters' ); var BLLRulerNode = require( 'BEERS_LAW_LAB/beerslaw/view/BLLRulerNode' ); var CuvetteNode = require( 'BEERS_LAW_LAB/beerslaw/view/CuvetteNode' ); var inherit = require( 'PHET_CORE/inherit' ); var LightNode = require( 'BEERS_LAW_LAB/beerslaw/view/LightNode' ); var Node = require( 'SCENERY/nodes/Node' ); var ResetAllButton = require( 'SCENERY_PHET/buttons/ResetAllButton' ); var ScreenView = require( 'JOIST/ScreenView' ); var SolutionControls = require( 'BEERS_LAW_LAB/beerslaw/view/SolutionControls' ); var WavelengthControls = require( 'BEERS_LAW_LAB/beerslaw/view/WavelengthControls' ); /** * @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; } } beersLawLab.register( 'BeersLawScreenView', BeersLawScreenView ); return inherit( ScreenView, BeersLawScreenView ); } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var ObjectIO = require( 'TANDEM/types/ObjectIO' ); var phetioInherit = require( 'TANDEM/phetioInherit' ); var ShakerParticle = require( 'BEERS_LAW_LAB/concentration/model/ShakerParticle' ); var ShakerParticleIO = require( 'BEERS_LAW_LAB/concentration/model/ShakerParticleIO' ); var Vector2 = require( 'DOT/Vector2' ); var validate = require( 'AXON/validate' ); /** * @param {ShakerParticles} shakerParticles * @param {string} phetioID * @constructor */ function ShakerParticlesIO( shakerParticles, phetioID ) { ObjectIO.call( this, shakerParticles, phetioID ); } phetioInherit( ObjectIO, 'ShakerParticlesIO', ShakerParticlesIO, {}, { documentation: 'Base type for a group of particles.', validator: { isValidValue: v => v instanceof phet.beersLawLab.ShakerParticles }, /** * Overrides the super type toStateObject (which was grabbing JSON for the entire instance) to return an empty instance. * The state is set through child instances and composition. * @returns {Object} */ toStateObject: function() {return {};}, /** * Clears the children from the model so it can be deserialized. * @param {ShakerParticles} shakerParticles */ clearChildInstances: function( shakerParticles ) { validate( shakerParticles, this.validator ); shakerParticles.removeAllParticles(); // Particles.step is not called in playback mode, so this needs to be called explicitly to update the view. shakerParticles.fireChanged(); }, /** * Create a dynamic particle as specified by the phetioID and state. * @param {ShakerParticles} shakerParticles * @param {Tandem} tandem * @param {Object} stateObject * @returns {ChargedParticle} */ addChildInstance: function( shakerParticles, tandem, stateObject ) { validate( shakerParticles, this.validator ); var value = ShakerParticleIO.fromStateObject( stateObject ); assert && assert( value.acceleration instanceof Vector2, 'acceleration should be a Vector2' ); shakerParticles.addParticle( new ShakerParticle( value.solute, value.location, value.orientation, value.velocity, value.acceleration, { tandem: tandem } ) ); // Particles.step is not called in playback mode, so this needs to be called explicitly to update the view. shakerParticles.fireChanged(); } } ); beersLawLab.register( 'ShakerParticlesIO', ShakerParticlesIO ); return ShakerParticlesIO; } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var BeersLawSolutionIO = require( 'BEERS_LAW_LAB/beerslaw/model/BeersLawSolutionIO' ); var BLLConstants = require( 'BEERS_LAW_LAB/common/BLLConstants' ); var BLLSymbols = require( 'BEERS_LAW_LAB/common/BLLSymbols' ); var Color = require( 'SCENERY/util/Color' ); var ColorRange = require( 'BEERS_LAW_LAB/common/model/ColorRange' ); var ConcentrationTransform = require( 'BEERS_LAW_LAB/beerslaw/model/ConcentrationTransform' ); var DerivedProperty = require( 'AXON/DerivedProperty' ); var inherit = require( 'PHET_CORE/inherit' ); var MolarAbsorptivityData = require( 'BEERS_LAW_LAB/beerslaw/model/MolarAbsorptivityData' ); var NumberProperty = require( 'AXON/NumberProperty' ); var PhetioObject = require( 'TANDEM/PhetioObject' ); var RangeWithValue = require( 'DOT/RangeWithValue' ); var Solvent = require( 'BEERS_LAW_LAB/common/model/Solvent' ); var StringUtils = require( 'PHETCOMMON/util/StringUtils' ); var Tandem = require( 'TANDEM/Tandem' ); var Util = require( 'DOT/Util' ); // strings var cobaltChlorideString = require( 'string!BEERS_LAW_LAB/cobaltChloride' ); var cobaltIINitrateString = require( 'string!BEERS_LAW_LAB/cobaltIINitrate' ); var copperSulfateString = require( 'string!BEERS_LAW_LAB/copperSulfate' ); var drinkMixString = require( 'string!BEERS_LAW_LAB/drinkMix' ); var nickelIIChlorideString = require( 'string!BEERS_LAW_LAB/nickelIIChloride' ); var pattern0Formula1NameString = require( 'string!BEERS_LAW_LAB/pattern.0formula.1name' ); var potassiumChromateString = require( 'string!BEERS_LAW_LAB/potassiumChromate' ); var potassiumDichromateString = require( 'string!BEERS_LAW_LAB/potassiumDichromate' ); var potassiumPermanganateString = require( 'string!BEERS_LAW_LAB/potassiumPermanganate' ); /** * @param {string} internalName - used internally, not displayed to the user * @param {string} name - name that is visible to the user * @param {string} formula - formula that is visible to the user * @param {MolarAbsorptivityData} molarAbsorptivityData * @param {RangeWithValue} concentrationRange * @param {ConcentrationTransform} concentrationTransform * @param {ColorRange} colorRange * @param {Object} [options] * @constructor */ function BeersLawSolution( internalName, name, formula, molarAbsorptivityData, concentrationRange, concentrationTransform, colorRange, options ) { assert && assert( internalName.indexOf( ' ' ) === -1, 'internalName cannot contain spaces: ' + internalName ); options = _.extend( { saturatedColor: colorRange.max, // {Color} color to use when the solution is saturated phetioType: BeersLawSolutionIO, tandem: Tandem.required }, options ); PhetioObject.call( this, options ); var self = this; // @public (read-only) this.solvent = Solvent.WATER; this.internalName = internalName; this.name = name; this.formula = formula; this.molarAbsorptivityData = molarAbsorptivityData; this.concentrationProperty = new NumberProperty( concentrationRange.defaultValue, { units: 'moles/liter', range: concentrationRange, tandem: options.tandem.createTandem( 'concentrationProperty' ) } ); this.concentrationRange = concentrationRange; this.concentrationTransform = concentrationTransform; this.colorRange = colorRange; this.saturatedColor = options.saturatedColor; // @public Solution color is derived from concentration this.fluidColorProperty = new DerivedProperty( [ this.concentrationProperty ], function( concentration ) { var color = self.solvent.colorProperty.get(); if ( concentration > 0 ) { var distance = Util.linear( self.concentrationRange.min, self.concentrationRange.max, 0, 1, concentration ); color = self.colorRange.interpolateLinear( distance ); } return color; } ); // @public - the name of the solution in tandem id format. Used to other make tandems that pertain to this solution. this.tandemName = options.tandem.tail; } beersLawLab.register( 'BeersLawSolution', BeersLawSolution ); inherit( PhetioObject, BeersLawSolution, { // @public reset: function() { this.concentrationProperty.reset(); }, // @public getDisplayName: function() { if ( this.formula === this.name ) { return this.name; } return StringUtils.format( pattern0Formula1NameString, this.formula, this.name ); } } ); // A new tandem instance is required here since the solutes are created statically. Signify that these solutions // are only used in the beers law screen by attaching them to that screen's tandem. var tandem = BLLConstants.BEERS_LAW_SCREEN_TANDEM.createTandem( 'solutions' ); //------------------------------------------------------------------------------------------- // Specific solutions below ... //------------------------------------------------------------------------------------------- BeersLawSolution.DRINK_MIX = new BeersLawSolution( 'drinkMix', drinkMixString, BLLSymbols.DRINK_MIX, MolarAbsorptivityData.DRINK_MIX, new RangeWithValue( 0, 0.400, 0.100 ), ConcentrationTransform.mM, new ColorRange( new Color( 255, 225, 225 ), Color.RED ), { tandem: tandem.createTandem( 'drinkMix' ) } ); BeersLawSolution.COBALT_II_NITRATE = new BeersLawSolution( 'cobaltIINitrate', cobaltIINitrateString, BLLSymbols.COBALT_II_NITRATE, MolarAbsorptivityData.COBALT_II_NITRATE, new RangeWithValue( 0, 0.400, 0.100 ), ConcentrationTransform.mM, new ColorRange( new Color( 255, 225, 225 ), Color.RED ), { tandem: tandem.createTandem( 'cobaltIINitrate' ) } ); BeersLawSolution.COBALT_CHLORIDE = new BeersLawSolution( 'cobaltChloride', cobaltChlorideString, BLLSymbols.COBALT_CHLORIDE, MolarAbsorptivityData.COBALT_CHLORIDE, new RangeWithValue( 0, 0.250, 0.100 ), ConcentrationTransform.mM, new ColorRange( new Color( 255, 242, 242 ), new Color( 255, 106, 106 ) ), { tandem: tandem.createTandem( 'cobaltChloride' ) } ); BeersLawSolution.POTASSIUM_DICHROMATE = new BeersLawSolution( 'potassiumDichromate', potassiumDichromateString, BLLSymbols.POTASSIUM_DICHROMATE, MolarAbsorptivityData.POTASSIUM_DICHROMATE, new RangeWithValue( 0, 0.000500, 0.000100 ), ConcentrationTransform.uM, new ColorRange( new Color( 255, 232, 210 ), new Color( 255, 127, 0 ) ), { tandem: tandem.createTandem( 'potassiumDichromate' ) } ); BeersLawSolution.POTASSIUM_CHROMATE = new BeersLawSolution( 'potassiumChromate', potassiumChromateString, BLLSymbols.POTASSIUM_CHROMATE, MolarAbsorptivityData.POTASSIUM_CHROMATE, new RangeWithValue( 0, 0.000400, 0.000100 ), ConcentrationTransform.uM, new ColorRange( new Color( 255, 255, 199 ), new Color( 255, 255, 0 ) ), { tandem: tandem.createTandem( 'potassiumChromate' ) } ); BeersLawSolution.NICKEL_II_CHLORIDE = new BeersLawSolution( 'nickelIIChloride', nickelIIChlorideString, BLLSymbols.NICKEL_II_CHLORIDE, MolarAbsorptivityData.NICKEL_II_CHLORIDE, new RangeWithValue( 0, 0.350, 0.100 ), ConcentrationTransform.mM, new ColorRange( new Color( 234, 244, 234 ), new Color( 0, 128, 0 ) ), { tandem: tandem.createTandem( 'nickelIIChloride' ) } ); BeersLawSolution.COPPER_SULFATE = new BeersLawSolution( 'copperSulfate', copperSulfateString, BLLSymbols.COPPER_SULFATE, MolarAbsorptivityData.COPPER_SULFATE, new RangeWithValue( 0, 0.200, 0.100 ), ConcentrationTransform.mM, new ColorRange( new Color( 222, 238, 255 ), new Color( 30, 144, 255 ) ), { tandem: tandem.createTandem( 'copperSulfate' ) } ); BeersLawSolution.POTASSIUM_PERMANGANATE = new BeersLawSolution( 'potassiumPermanganate', potassiumPermanganateString, BLLSymbols.POTASSIUM_PERMANGANATE, MolarAbsorptivityData.POTASSIUM_PERMANGANATE, new RangeWithValue( 0, 0.000800, 0.000100 ), ConcentrationTransform.uM, new ColorRange( new Color( 255, 235, 255 ), new Color( 255, 0, 255 ) ), { tandem: tandem.createTandem( 'potassiumPermanganate' ), // has a special saturated color saturatedColor: new Color( 80, 0, 120 ) } ); return BeersLawSolution; } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var ObjectIO = require( 'TANDEM/types/ObjectIO' ); var phetioInherit = require( 'TANDEM/phetioInherit' ); var StringIO = require( 'TANDEM/types/StringIO' ); var VoidIO = require( 'TANDEM/types/VoidIO' ); var validate = require( 'AXON/validate' ); // ifphetio var phetioEngine = require( 'ifphetio!PHET_IO/phetioEngine' ); /** * @param {Solute} solute * @param {string} phetioID * @constructor */ function SoluteIO( solute, phetioID ) { ObjectIO.call( this, solute, phetioID ); } phetioInherit( ObjectIO, 'SoluteIO', SoluteIO, { setName: { returnType: VoidIO, parameterTypes: [ StringIO ], implementation: function( text ) { this.instance.name = text; }, documentation: 'Set the name of the solute', invocableForReadOnlyElements: false }, setFormula: { returnType: VoidIO, parameterTypes: [ StringIO ], implementation: function( text ) { this.instance.formula = text; }, documentation: 'Set the formula of the solute', invocableForReadOnlyElements: false } }, { documentation: 'The Solute for the sim.', validator: { isValidValue: v => v instanceof phet.beersLawLab.Solute }, /** * Serializes an instance. * @param {Solute} solute * @returns {Object} */ toStateObject: function( solute ) { validate( solute, this.validator ); return solute.tandem.phetioID; }, /** * Deserializes an instance. * @param {Object} stateObject * @returns {Solute} */ fromStateObject: function( stateObject ) { return phetioEngine.getPhetioObject( stateObject ); } } ); beersLawLab.register( 'SoluteIO', SoluteIO ); return SoluteIO; } );
define( function( require ) { 'use strict'; // modules var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var Dimension2 = require( 'DOT/Dimension2' ); var DynamicProperty = require( 'AXON/DynamicProperty' ); var HBox = require( 'SCENERY/nodes/HBox' ); var HStrut = require( 'SCENERY/nodes/HStrut' ); var inherit = require( 'PHET_CORE/inherit' ); var LinearGradient = require( 'SCENERY/util/LinearGradient' ); var NumberControl = require( 'SCENERY_PHET/NumberControl' ); var PhetFont = require( 'SCENERY_PHET/PhetFont' ); var Property = require( 'AXON/Property' ); var Range = require( 'DOT/Range' ); var StringUtils = require( 'PHETCOMMON/util/StringUtils' ); var SunConstants = require( 'SUN/SunConstants' ); var Text = require( 'SCENERY/nodes/Text' ); var Util = require( 'DOT/Util' ); // strings var concentrationString = require( 'string!BEERS_LAW_LAB/concentration' ); var pattern0LabelString = require( 'string!BEERS_LAW_LAB/pattern.0label' ); var pattern0Value1UnitsString = require( 'string!BEERS_LAW_LAB/pattern.0value.1units' ); // constants var FONT = new PhetFont( 20 ); var TICK_FONT = new PhetFont( 16 ); var SLIDER_INTERVAL = 5; // in view units /** * @param {BeersLawSolution} solution * @param {Object} [options] * @constructor */ function ConcentrationControl( solution, options ) { options = _.extend( { // NumberControl options titleNodeOptions: { font: FONT }, numberDisplayOptions: { font: FONT, minBackgroundWidth: 95 // determined empirically }, arrowButtonOptions: { scale: 1, touchAreaXDilation: 8, touchAreaYDilation: 15 }, // Slider options, passed through by NumberControl sliderOptions: { trackSize: new Dimension2( 200, 15 ), thumbSize: new Dimension2( 22, 45 ), constrainValue: function( value ) { return Util.roundToInterval( value, SLIDER_INTERVAL ); } }, // single-line horizontal layout layoutFunction: function( titleNode, numberDisplay, slider, leftArrowButton, rightArrowButton ) { return new HBox( { spacing: 5, children: [ titleNode, numberDisplay, new HStrut( 5 ), leftArrowButton, slider, rightArrowButton ] } ); } }, options ); // @public (read-only) this.solution = solution; var transform = solution.concentrationTransform; var title = StringUtils.format( pattern0LabelString, concentrationString ); // e.g. display units that are specific to the solution, e.g. '{0} mM' assert && assert( !options.numberDisplayOptions.valuePattern, 'ConcentrationControl sets valuePattern' ); options.numberDisplayOptions.valuePattern = StringUtils.format( pattern0Value1UnitsString, SunConstants.VALUE_NUMBERED_PLACEHOLDER, transform.units ); assert && assert( options.delta === undefined, 'ConcentrationControl sets delta' ); options.delta = 1; // in view coordinates // fill the track with a linear gradient that corresponds to the solution color assert && assert( !options.sliderOptions.trackFillEnabled, 'ConcentrationControl sets trackFillEnabled' ); options.sliderOptions.trackFillEnabled = new LinearGradient( 0, 0, options.sliderOptions.trackSize.width, 0 ) .addColorStop( 0, solution.colorRange.min ) .addColorStop( 1, solution.colorRange.max ); // map concentration value between model and view var numberProperty = new DynamicProperty( new Property( solution.concentrationProperty ), { bidirectional: true, // Necessary because bidirectional:true reentrant: true, // map from model to view, apply options.interval to model value map: value => transform.modelToView( value ), // map from view to model, apply options.interval to model value inverseMap: value => transform.viewToModel( value ) } ); // convert solution's concentration range from model to view var numberRange = new Range( transform.modelToView( solution.concentrationRange.min ), transform.modelToView( solution.concentrationRange.max ) ); // ticks at the min and max of the solution's concentration range assert && assert( !options.sliderOptions.majorTicks, 'ConcentrationControl sets majorTicks' ); options.sliderOptions.majorTicks = []; [ numberRange.min, numberRange.max ].forEach( function( value ) { options.sliderOptions.majorTicks.push( { value: value, label: new Text( value, { font: TICK_FONT } ) } ); } ); NumberControl.call( this, title, numberProperty, numberRange, options ); } beersLawLab.register( 'ConcentrationControl', ConcentrationControl ); return inherit( NumberControl, ConcentrationControl ); } );
define( function( require ) { 'use strict'; // modules var AquaRadioButton = require( 'SUN/AquaRadioButton' ); var beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); var BLLConstants = require( 'BEERS_LAW_LAB/common/BLLConstants' ); var BooleanProperty = require( 'AXON/BooleanProperty' ); var HBox = require( 'SCENERY/nodes/HBox' ); var HStrut = require( 'SCENERY/nodes/HStrut' ); var inherit = require( 'PHET_CORE/inherit' ); var Node = require( 'SCENERY/nodes/Node' ); var Panel = require( 'SUN/Panel' ); var PhetFont = require( 'SCENERY_PHET/PhetFont' ); var Rectangle = require( 'SCENERY/nodes/Rectangle' ); var StringUtils = require( 'PHETCOMMON/util/StringUtils' ); var Text = require( 'SCENERY/nodes/Text' ); var Util = require( 'DOT/Util' ); var VBox = require( 'SCENERY/nodes/VBox' ); var WavelengthSlider = require( 'SCENERY_PHET/WavelengthSlider' ); // strings var pattern0LabelString = require( 'string!BEERS_LAW_LAB/pattern.0label' ); var pattern0Value1UnitsString = require( 'string!BEERS_LAW_LAB/pattern.0value.1units' ); var presetString = require( 'string!BEERS_LAW_LAB/preset' ); var unitsNmString = require( 'string!BEERS_LAW_LAB/units.nm' ); var variableString = require( 'string!BEERS_LAW_LAB/variable' ); var wavelengthString = require( 'string!BEERS_LAW_LAB/wavelength' ); /** * @param {Property.<BeersLawSolution>} solutionProperty * @param {Light} light * @param {Tandem} tandem * @constructor */ function WavelengthControls( solutionProperty, light, tandem ) { // @private is the wavelength variable or fixed? this.variableWavelengthProperty = new BooleanProperty( false, { tandem: tandem.createTandem( 'variableWavelengthProperty' ) } ); var xMargin = 7; var yMargin = 3; var label = new Text( StringUtils.format( pattern0LabelString, wavelengthString ), { font: new PhetFont( 20 ), fill: 'black', tandem: tandem.createTandem( 'label' ) } ); var valueDisplay = new Text( formatWavelength( light.wavelengthProperty.get() ), { font: new PhetFont( 20 ), fill: 'black', y: label.y, // align baselines tandem: tandem.createTandem( 'valueDisplay' ) } ); var valueBackground = new Rectangle( 0, 0, valueDisplay.width + xMargin + xMargin, valueDisplay.height + yMargin + yMargin, { fill: 'white', stroke: 'lightGray', left: label.right + 10, centerY: valueDisplay.centerY } ); valueDisplay.right = valueBackground.right - xMargin; // right aligned var valueParent = new Node( { children: [ label, valueBackground, valueDisplay ], maxWidth: 250 // constrain width for i18n } ); // preset var presetRadioButton = new AquaRadioButton( this.variableWavelengthProperty, false, new Text( presetString, { font: new PhetFont( 18 ), fill: 'black' } ), { radius: BLLConstants.RADIO_BUTTON_RADIUS, tandem: tandem.createTandem( 'presetWavelengthRadioButton' ) } ); presetRadioButton.touchArea = presetRadioButton.localBounds.dilatedXY( 6, 8 ); // variable var variableRadioButton = new AquaRadioButton( this.variableWavelengthProperty, true, new Text( variableString, { font: new PhetFont( 18 ), fill: 'black' } ), { radius: BLLConstants.RADIO_BUTTON_RADIUS, tandem: tandem.createTandem( 'variableWavelengthRadioButton' ) } ); variableRadioButton.touchArea = variableRadioButton.localBounds.dilatedXY( 6, 8 ); var radioButtons = new HBox( { spacing: 18, maxWidth: 250, // constrain width for i18n children: [ presetRadioButton, variableRadioButton ] } ); var wavelengthSlider = new WavelengthSlider( light.wavelengthProperty, { trackWidth: 150, trackHeight: 30, valueVisible: false, tweakersTouchAreaXDilation: 10, tweakersTouchAreaYDilation: 10, tandem: tandem.createTandem( 'wavelengthSlider' ) } ); // rendering order var content = new VBox( { spacing: 15, align: 'left', children: [ valueParent, radioButtons, wavelengthSlider ] } ); // add a horizontal strut to prevent width changes content.addChild( new HStrut( Math.max( content.width, wavelengthSlider.width ) ) ); Panel.call( this, content, { xMargin: 20, yMargin: 20, fill: '#F0F0F0', stroke: 'gray', lineWidth: 1, tandem: tandem } ); // When the radio button selection changes... this.variableWavelengthProperty.link( function( isVariable ) { // add/remove the slider so that the panel resizes if ( isVariable ) { !content.hasChild( wavelengthSlider ) && content.addChild( wavelengthSlider ); } else { content.hasChild( wavelengthSlider ) && content.removeChild( wavelengthSlider ); } if ( !isVariable ) { // Set the light to the current solution's lambdaMax wavelength. light.wavelengthProperty.set( solutionProperty.get().molarAbsorptivityData.lambdaMax ); } } ); // sync displayed value with model light.wavelengthProperty.link( function( wavelength ) { valueDisplay.text = formatWavelength( wavelength ); valueDisplay.right = valueBackground.right - xMargin; // right aligned } ); } beersLawLab.register( 'WavelengthControls', WavelengthControls ); var formatWavelength = function( wavelength ) { return StringUtils.format( pattern0Value1UnitsString, Util.toFixed( wavelength, 0 ), unitsNmString ); }; return inherit( Panel, WavelengthControls, { // @public reset: function() { this.variableWavelengthProperty.reset(); } } ); } );
define( function( require ) { 'use strict'; // modules const beersLawLab = require( 'BEERS_LAW_LAB/beersLawLab' ); const ComboBox = require( 'SUN/ComboBox' ); const ComboBoxItem = require( 'SUN/ComboBoxItem' ); const HBox = require( 'SCENERY/nodes/HBox' ); const PhetFont = require( 'SCENERY_PHET/PhetFont' ); const Rectangle = require( 'SCENERY/nodes/Rectangle' ); const RichText = require( 'SCENERY/nodes/RichText' ); const StringUtils = require( 'PHETCOMMON/util/StringUtils' ); const Text = require( 'SCENERY/nodes/Text' ); // strings const pattern0LabelString = require( 'string!BEERS_LAW_LAB/pattern.0label' ); const solutionString = require( 'string!BEERS_LAW_LAB/solution' ); class SolutionComboBox extends ComboBox { /** * @param {BeersLawSolution[]} solutions * @param {Property.<BeersLawSolution>} selectedSolutionProperty * @param {Node} solutionListParent * @param {Tandem} tandem * @constructor */ constructor( solutions, selectedSolutionProperty, solutionListParent, tandem ) { // 'Solution' label const label = new Text( StringUtils.format( pattern0LabelString, solutionString ), { font: new PhetFont( 20 ) } ); // items const items = solutions.map( solution => createItem( solution, tandem ) ); super( items, selectedSolutionProperty, solutionListParent, { labelNode: label, listPosition: 'above', xMargin: 12, yMargin: 12, highlightFill: 'rgb( 218, 255, 255 )', cornerRadius: 8, tandem: tandem } ); } } beersLawLab.register( 'SolutionComboBox', SolutionComboBox ); /** * Creates a combo box item. * @private * @param {BeersLawSolution} solution * @param {Tandem} tandem * @returns {ComboBoxItem} */ const createItem = function( solution, tandem ) { const colorSquare = new Rectangle( 0, 0, 20, 20, { fill: solution.saturatedColor, stroke: solution.saturatedColor.darkerColor() } ); const solutionName = new RichText( solution.getDisplayName(), { font: new PhetFont( 20 ), tandem: tandem.createTandem( solution.tandemName + 'Text' ) } ); const hBox = new HBox( { spacing: 5, children: [ colorSquare, solutionName ] } ); return new ComboBoxItem( hBox, solution, { tandemName: solution.tandemName } ); }; return SolutionComboBox; } );