debugPositions: function( johnTravoltageView ) { johnTravoltageView.touchArea = Shape.rectangle( 0, 0, 1000, 1000 ); johnTravoltageView.mouseArea = Shape.rectangle( 0, 0, 1000, 1000 ); var string = ''; johnTravoltageView.addInputListener( { down: function( event ) { var pt = event.pointer.point; var global = johnTravoltageView.globalToLocalPoint( pt ); var a = 'new Vector2(' + global.x + ',' + global.y + '),\n'; string = string + a; console.log( string ); } } ); },
/** * The slider thumb, a rounded rectangle with a horizontal line through its center. Origin is at the thumb's geometric center. * @param {Dimension2} size * @param {Property.<number>} property * @param {Range} valueRange * @param {number} decimalPlaces * @param {Range} positionRange * @constructor */ function Thumb( size, property, valueRange, decimalPlaces, positionRange ) { var thisNode = this; Node.call( this ); // nodes var bodyNode = new Rectangle( -size.width / 2, -size.height / 2, size.width, size.height, 10, 10, { fill: THUMB_NORMAL_COLOR, stroke: THUMB_STROKE_COLOR, lineWidth: 1 } ); var centerLineNode = new Path( Shape.lineSegment( -( size.width / 2 ) + 3, 0, ( size.width / 2 ) - 3, 0 ), { stroke: THUMB_CENTER_LINE_COLOR } ); // rendering order thisNode.addChild( bodyNode ); thisNode.addChild( centerLineNode ); // touch area var touchXMargin = 0 * bodyNode.width; // thumb seems wide enough, so zero for now var touchYMargin = 1 * bodyNode.height; // expand height since thumb is not very tall and drag direction is vertical bodyNode.touchArea = Shape.rectangle( bodyNode.left - touchXMargin, bodyNode.top - touchYMargin, bodyNode.width + ( 2 * touchXMargin ), bodyNode.height + ( 2 * touchYMargin ) ); // interactivity thisNode.cursor = 'pointer'; thisNode.addInputListener( new ThumbDragHandler( thisNode, property, valueRange, decimalPlaces, positionRange ) ); bodyNode.addInputListener( new FillHighlightListener( THUMB_NORMAL_COLOR, THUMB_HIGHLIGHT_COLOR ) ); }
/** * @param {Function} callback function to be called when the user presses the clear thermal button. * @param {Skater} skater the model's skater model * @param {*}options * @constructor */ function ClearThermalButton( callback, skater, options ) { var clearThermalButton = this; options = _.extend( { cursor: 'pointer', phetioID: null }, options ); var icon = new Image( trashCanImage, { scale: 0.22 } ); RectangularPushButton.call( this, { content: icon, baseColor: new Color( 230, 230, 240 ), disabledBaseColor: 'white', cornerRadius: 6, listener: callback, buttonAppearanceStrategy: RectangularButtonView.flatAppearanceStrategy, xMargin: 7, yMargin: 3, phetioID: options.phetioID } ); skater.allowClearingThermalEnergyProperty.link( function( allowClearingThermalEnergy ) { icon.image = allowClearingThermalEnergy ? trashCanImage : trashCanGrayImage; icon.opacity = allowClearingThermalEnergy ? 1 : 0.3; clearThermalButton.pickable = allowClearingThermalEnergy; } ); this.mouseArea = this.touchArea = Shape.rectangle( icon.bounds.minX, icon.bounds.minY, icon.bounds.width, icon.bounds.height ); this.mutate( options ); }
solution.volumeProperty.link( function( volume ) { if ( volume === 0 ) { thisNode.clipArea = null; } else { var solutionHeight = beakerBounds.getHeight() * volume / beaker.volume; thisNode.clipArea = Shape.rectangle( beakerBounds.minX, beakerBounds.maxY - solutionHeight, beakerBounds.getWidth(), solutionHeight ); } thisNode.moleculesNode.invalidatePaint(); //WORKAROUND: #25, scenery#200 } );
QUnit.test( 'ES5 Setter / Getter tests', function( assert ) { var node = new Path( null ); var fill = '#abcdef'; node.fill = fill; assert.equal( node.fill, fill ); assert.equal( node.getFill(), fill ); var otherNode = new Path( Shape.rectangle( 0, 0, 10, 10 ), { fill: fill } ); assert.equal( otherNode.fill, fill ); } );
QUnit.test( 'Sceneless node handling', function( assert ) { var a = new Path( null ); var b = new Path( null ); var c = new Path( null ); a.setShape( Shape.rectangle( 0, 0, 20, 20 ) ); c.setShape( Shape.rectangle( 10, 10, 30, 30 ) ); a.addChild( b ); b.addChild( c ); a.validateBounds(); a.removeChild( b ); c.addChild( a ); b.validateBounds(); assert.ok( true, 'so we have at least 1 test in this set' ); } );
debugLineSegments: function( johnTravoltageView ) { johnTravoltageView.touchArea = Shape.rectangle( 0, 0, 1000, 1000 ); johnTravoltageView.mouseArea = Shape.rectangle( 0, 0, 1000, 1000 ); var string = ''; var p1 = null; johnTravoltageView.addInputListener( { down: function( event ) { var pt = event.pointer.point; var global = johnTravoltageView.globalToLocalPoint( pt ); if ( p1 ) { string = string + 'new LineSegment(' + p1.x + ',' + p1.y + ',' + global.x + ',' + global.y + '),\n'; console.log( string ); p1 = null; } else { p1 = global; } } } ); }
/** * Meter probe, origin at center of crosshairs. * @param {Movable} probe * @param {ModelViewTransform2} modelViewTransform * @param {Node} solutionNode * @param {Node} dropperFluidNode * @param {Node} waterFluidNode * @param {Node} drainFluidNode * @constructor */ function ProbeNode( probe, modelViewTransform, solutionNode, dropperFluidNode, waterFluidNode, drainFluidNode ) { var thisNode = this; Node.call( thisNode, { cursor: 'pointer' } ); // probe image file var imageNode = new Image( probeImage ); thisNode.addChild( imageNode ); var radius = imageNode.height / 2; // assumes that image height defines the radius imageNode.x = -imageNode.width + radius; // assumes the image is oriented with probe 'handle' on left imageNode.y = -radius; // assumes the image is oriented horizontally // probe location probe.locationProperty.link( function( location ) { thisNode.translation = modelViewTransform.modelToViewPosition( location ); } ); // touch area var dx = 0.25 * imageNode.width; var dy = 0.25 * imageNode.height; thisNode.touchArea = Shape.rectangle( imageNode.x - dx, imageNode.y - dy, imageNode.width + dx + dx, imageNode.height + dy + dy ); // drag handler thisNode.addInputListener( new MovableDragHandler( probe, modelViewTransform ) ); var isInNode = function( node ) { return node.getBounds().containsPoint( probe.locationProperty.get() ); }; thisNode.isInSolution = function() { return isInNode( solutionNode ); }; thisNode.isInWater = function() { return isInNode( waterFluidNode ); }; thisNode.isInDrainFluid = function() { return isInNode( drainFluidNode ); }; thisNode.isInDropperSolution = function() { return isInNode( dropperFluidNode ); }; }
QUnit.test( 'Setting left/right of node', function( assert ) { var node = new Path( Shape.rectangle( -20, -20, 50, 50 ), { scale: 2 } ); equalsApprox( assert, node.left, -40 ); equalsApprox( assert, node.right, 60 ); node.left = 10; equalsApprox( assert, node.left, 10 ); equalsApprox( assert, node.right, 110 ); node.right = 10; equalsApprox( assert, node.left, -90 ); equalsApprox( assert, node.right, 10 ); node.centerX = 5; equalsApprox( assert, node.centerX, 5 ); equalsApprox( assert, node.left, -45 ); equalsApprox( assert, node.right, 55 ); } );
/** * * @param {Property.<boolean>} showValues * @param {Property.<boolean>} visible * @param {number} verticalSpacingForCaptions * @param {boolean} showShowValuesCheckbox * @constructor */ function ConcentrationBarChart( showValues, visible, verticalSpacingForCaptions, showShowValuesCheckbox ) { var self = this; Node.call( self ); //@protected Background for the bar chart self.background = new Path( Shape.rectangle( 0, 0, 180, 170 + verticalSpacingForCaptions ), { fill: SugarAndSaltSolutionsConstants.WATER_COLOR//Background for the bar chart } ); self.addChild( self.background ); self.barChartContentNode = new Node(); self.addChild( self.barChartContentNode ); //The x-axis, the baseline for the bars //@protected self.abscissaY = self.background.bounds.getHeight() - 60 - verticalSpacingForCaptions; self.barChartContentNode.addChild( new Path( Shape.lineSegment( 0, self.abscissaY, self.background.bounds.getWidth(), self.abscissaY ), { lineWidth: 1, stroke: Color.BLACK } ) ); //Add a checkbox that lets the user toggle on and off whether actual values are shown //It is only shown in the first tab, since values are suppressed in the Micro tab if ( showShowValuesCheckbox ) { var showValuesCheckbox = new Checkbox( new Text( showValuesString, { font: SugarAndSaltSolutionsConstants.CONTROL_FONT } ), showValues, { boxWidth: 20 } ); self.barChartContentNode.addChild( showValuesCheckbox ); showValuesCheckbox.x = self.bounds.getWidth() / 2 - showValuesCheckbox.bounds.width / 2; showValuesCheckbox.y = self.getHeight() - showValuesCheckbox.bounds.getHeight() - INSET; } //Only show this bar chart if the user has opted to do so visible.linkAttribute( self, 'visible' ); }
/** * RadioButtonGroup constructor. * * @param {Property} property * @param {Array} contentArray an array of objects that have two keys each: value and node the node key holds a * scenery Node that is the content for a given radio button. the value key should hold the value that the property * takes on for the corresponding node to be selected. Optionally, these objects can have an attribute 'label', which * is a {Node} used to label the button. You can also pass some specific a11y options * (labelContent/descriptionContent) through, see "new RadioButtonGroupMember" construction. * @param {Object} [options] * @constructor */ function RadioButtonGroup( property, contentArray, options ) { options = _.extend( { tandem: Tandem.required, // a11y tagName: 'ul', labelTagName: 'h3', ariaRole: 'radiogroup', groupFocusHighlight: true }, options ); // increment instance count instanceCount++; assert && assert( !options.hasOwnProperty( 'children' ), 'Cannot pass in children to a RadioButtonGroup, ' + 'create siblings in the parent node instead' ); // make sure every object in the content array has properties 'node' and 'value' assert && assert( _.every( contentArray, function( obj ) { return obj.hasOwnProperty( 'node' ) && obj.hasOwnProperty( 'value' ); } ), 'contentArray must be an array of objects with properties "node" and "value"' ); var i; // for loops // make sure that each value passed into the contentArray is unique var uniqueValues = []; for ( i = 0; i < contentArray.length; i++ ) { if ( uniqueValues.indexOf( contentArray[ i ].value ) < 0 ) { uniqueValues.push( contentArray[ i ].value ); } else { throw new Error( 'Duplicate value: "' + contentArray[ i ].value + '" passed into RadioButtonGroup.js' ); } } // make sure that the property passed in currently has a value from the contentArray if ( uniqueValues.indexOf( property.get() ) === -1 ) { throw new Error( 'The property passed in to RadioButtonGroup has an illegal value "' + property.get() + '" that is not present in the contentArray' ); } var defaultOptions = { // LayoutBox options (super class of RadioButtonGroup) spacing: 10, orientation: 'vertical', enabledProperty: new Property( true ), // whether or not the set of radio buttons as a whole is enabled // The fill for the rectangle behind the radio buttons. Default color is bluish color, as in the other button library. baseColor: ColorConstants.LIGHT_BLUE, disabledBaseColor: ColorConstants.LIGHT_GRAY, // Opacity can be set separately for the buttons and button content. selectedButtonOpacity: 1, deselectedButtonOpacity: 0.6, selectedContentOpacity: 1, deselectedContentOpacity: 0.6, overButtonOpacity: 0.8, overContentOpacity: 0.8, selectedStroke: 'black', deselectedStroke: new Color( 50, 50, 50 ), selectedLineWidth: 1.5, deselectedLineWidth: 1, // The following options specify highlight behavior overrides, leave as null to get the default behavior // Note that highlighting applies only to deselected buttons overFill: null, overStroke: null, overLineWidth: null, // These margins are *within* each button buttonContentXMargin: 5, buttonContentYMargin: 5, // alignment of the content nodes *within* each button buttonContentXAlign: 'center', // {string} see BUTTON_CONTENT_X_ALIGN_VALUES buttonContentYAlign: 'center', // {string} see BUTTON_CONTENT_Y_ALIGN_VALUES // TouchArea expansion touchAreaXDilation: 0, touchAreaYDilation: 0, // MouseArea expansion mouseAreaXDilation: 0, mouseAreaYDilation: 0, //The radius for each button cornerRadius: 4, // How far from the button the text label is (only applies if labels are passed in) labelSpacing: 0, // Which side of the button the label will appear, options are 'top', 'bottom', 'left', 'right' // (only applies if labels are passed in) labelAlign: 'bottom', // The default appearances use the color values specified above, but other appearances could be specified for more // customized behavior. Generally setting the color values above should be enough to specify the desired look. buttonAppearanceStrategy: RadioButtonGroupAppearance.defaultRadioButtonsAppearance, contentAppearanceStrategy: RadioButtonGroupAppearance.contentAppearanceStrategy, // a11y - focus highlight expansion a11yHighlightXDilation: 0, a11yHighlightYDilation: 0 }; options = _.extend( _.clone( defaultOptions ), options ); assert && assert( _.includes( BUTTON_CONTENT_X_ALIGN_VALUES, options.buttonContentXAlign ), 'invalid buttonContentXAlign: ' + options.buttonContentXAlign ); assert && assert( _.includes( BUTTON_CONTENT_Y_ALIGN_VALUES, options.buttonContentYAlign ), 'invalid buttonContentYAlign: ' + options.buttonContentYAlign ); // make a copy of the options to pass to individual buttons that includes all default options but not scenery options var buttonOptions = _.pick( options, _.keys( defaultOptions ) ); // calculate the maximum width and height of the content so we can make all radio buttons the same size var widestContentWidth = _.maxBy( contentArray, function( content ) { return content.node.width; } ).node.width; var tallestContentHeight = _.maxBy( contentArray, function( content ) { return content.node.height; } ).node.height; // make sure all radio buttons are the same size and create the RadioButtons var buttons = []; var button; for ( i = 0; i < contentArray.length; i++ ) { var currentContent = contentArray[ i ]; assert && assert( !currentContent.hasOwnProperty( 'phetioType' ), 'phetioType should be provided by ' + 'the property passed to the ' + 'RadioButtonGroup constructor' ); assert && assert( !currentContent.tandem, 'content arrays should not have tandem instances, they should use ' + 'tandemName instead' ); var opts = _.extend( { content: currentContent.node, xMargin: options.buttonContentXMargin, yMargin: options.buttonContentYMargin, xAlign: options.buttonContentXAlign, yAlign: options.buttonContentYAlign, minWidth: widestContentWidth + 2 * options.buttonContentXMargin, minHeight: tallestContentHeight + 2 * options.buttonContentYMargin, phetioDocumentation: currentContent.phetioDocumentation || '' }, buttonOptions ); // Pass through the tandem given the tandemName, but also support uninstrumented simulations if ( currentContent.tandemName ) { opts.tandem = options.tandem.createTandem( currentContent.tandemName ); } // a11y create the label for the radio button if ( currentContent.labelContent ) { opts.labelContent = currentContent.labelContent; } // a11y create description for radio button // use if block to prevent empty 'p' tag being added when no option is present if ( currentContent.descriptionContent ) { opts.descriptionContent = currentContent.descriptionContent; } var radioButton = new RadioButtonGroupMember( property, currentContent.value, opts ); // a11y - so the browser recognizes these buttons are in the same group, see instanceCount for more info radioButton.setAccessibleAttribute( 'name', CLASS_NAME + instanceCount ); // ensure the buttons don't resize when selected vs unselected by adding a rectangle with the max size var maxLineWidth = Math.max( options.selectedLineWidth, options.deselectedLineWidth ); var maxButtonWidth = maxLineWidth + widestContentWidth + options.buttonContentXMargin * 2; var maxButtonHeight = maxLineWidth + tallestContentHeight + options.buttonContentYMargin * 2; var boundingRect = new Rectangle( 0, 0, maxButtonWidth, maxButtonHeight, { fill: 'rgba(0,0,0,0)', center: radioButton.center } ); radioButton.addChild( boundingRect ); // default bounds for focus highlight, will include label if one exists var defaultHighlightBounds = null; // if a label is given, the button becomes a LayoutBox with the label and button if ( currentContent.label ) { var label = currentContent.label; var labelOrientation = ( options.labelAlign === 'bottom' || options.labelAlign === 'top' ) ? 'vertical' : 'horizontal'; var labelChildren = ( options.labelAlign === 'left' || options.labelAlign === 'top' ) ? [ label, radioButton ] : [ radioButton, label ]; button = new LayoutBox( { children: labelChildren, spacing: options.labelSpacing, orientation: labelOrientation } ); var xDilation = options.touchAreaXDilation; var yDilation = options.touchAreaYDilation; // override the touch and mouse areas defined in RectangularButtonView // extra width is added to the SingleRadioButtons so they don't change size if the line width changes, // that is why lineWidth is subtracted from the width and height when calculating these new areas radioButton.touchArea = Shape.rectangle( -xDilation, -yDilation, button.width + 2 * xDilation - maxLineWidth, button.height + 2 * yDilation - maxLineWidth ); xDilation = options.mouseAreaXDilation; yDilation = options.mouseAreaYDilation; radioButton.mouseArea = Shape.rectangle( -xDilation, -yDilation, button.width + 2 * xDilation - maxLineWidth, button.height + 2 * yDilation - maxLineWidth ); // make sure the label mouse and touch areas don't block the expanded button touch and mouse areas label.pickable = false; // use the same content appearance strategy for the labels that is used for the button content options.contentAppearanceStrategy( label, radioButton.interactionStateProperty, options ); // a11y - include label in focus highlight defaultHighlightBounds = radioButton.mouseArea.bounds.dilated( 5 ); } else { button = radioButton; defaultHighlightBounds = button.bounds.dilated( FocusHighlightPath.getDilationCoefficient( button ) ); } // a11y - set the focus highlight, dilated by the optional expansion values var highlightBounds = defaultHighlightBounds.dilatedX( opts.a11yHighlightXDilation ).dilatedY( opts.a11yHighlightYDilation ); radioButton.setFocusHighlight( Shape.bounds( highlightBounds ) ); buttons.push( button ); } // @private this.enabledProperty = options.enabledProperty; // super call options.children = buttons; LayoutBox.call( this, options ); var self = this; // a11y - this node's primary sibling is aria-labelledby its own label so the label content is read whenever // a member of the group receives focus this.addAriaLabelledbyAssociation( { thisElementName: AccessiblePeer.PRIMARY_SIBLING, otherNode: this, otherElementName: AccessiblePeer.LABEL_SIBLING } ); // When the entire RadioButtonGroup gets disabled, gray them out and make them unpickable (and vice versa) var enabledListener = function( isEnabled ) { self.pickable = isEnabled; for ( i = 0; i < contentArray.length; i++ ) { if ( buttons[ i ] instanceof LayoutBox ) { for ( var j = 0; j < 2; j++ ) { buttons[ i ].children[ j ].enabled = isEnabled; } } else { buttons[ i ].enabled = isEnabled; } } }; this.enabledProperty.link( enabledListener ); // make the unselected buttons pickable and have a pointer cursor var propertyListener = function( value ) { if ( self.enabledProperty.get() ) { for ( i = 0; i < contentArray.length; i++ ) { if ( contentArray[ i ].value === value ) { buttons[ i ].pickable = false; buttons[ i ].cursor = null; } else { buttons[ i ].pickable = true; buttons[ i ].cursor = 'pointer'; } } } }; property.link( propertyListener ); // @private - remove listeners from buttons and make eligible for garbage collection this.disposeRadioButtonGroup = function() { self.enabledProperty.unlink( enabledListener ); property.unlink( propertyListener ); // dispose all buttons for ( i = 0; i < contentArray.length; i++ ) { buttons[ i ].dispose(); } }; // a11y - register component for binder docs assert && phet.chipper.queryParameters.binder && InstanceRegistry.registerDataURL( 'sun', 'RadioButtonGroup', 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; } }
/** * @param {GQModel} model * @param {GQViewProperties} viewProperties * @param {Object} [options] */ constructor( model, viewProperties, options ) { options = _.extend( { // prevent a parabola's vertex and equation from overlapping preventVertexAndEquationOverlap: true, // {Node[]}, other curves to be displayed (terms, directrix, axis of symmetry) rendered in the order provided otherCurves: DEFAULT_OTHER_CURVES, // {Node[]}, decorations (manipulators, roots,...) rendered in the order provided decorations: DEFAULT_DECORATIONS }, options ); assert && assert( !options.tandem, 'GQGraphNode should not be instrumented' ); super( options ); // Cartesian coordinates graph const graphNode = new GraphNode( model.graph, model.modelViewTransform ); // Interactive quadratic curve const interactiveQuadraticNode = new QuadraticNode( model.quadraticProperty, model.graph.xRange, model.graph.yRange, model.modelViewTransform, viewProperties.equationForm, viewProperties.equationsVisibleProperty, { lineWidth: GQConstants.INTERACTIVE_QUADRATIC_LINE_WIDTH, preventVertexAndEquationOverlap: options.preventVertexAndEquationOverlap } ); // {QuadraticNode|null} the saved line let savedLineNode = null; // Parent for other lines, e.g. quadratic terms, directrix, axis of symmetry const otherCurvesLayer = new Node( { children: options.otherCurves } ); // Parent for decorations, e.g. vertex, roots, manipulators const decorationsLayer = new Node( { children: options.decorations } ); // All lines, clipped to the graph const allLinesParent = new Node( { clipArea: Shape.rectangle( model.graph.xRange.min, model.graph.yRange.min, model.graph.xRange.getLength(), model.graph.yRange.getLength() ).transformed( model.modelViewTransform.getMatrix() ), children: [ otherCurvesLayer, interactiveQuadraticNode ] } ); // Everything that's on the graph const contentParent = new Node( { children: [ allLinesParent, decorationsLayer ] } ); // rendering order this.addChild( graphNode ); this.addChild( contentParent ); // When the saved quadratic changes... model.savedQuadraticProperty.link( savedQuadratic => { // remove and dispose any previously-saved line if ( savedLineNode ) { allLinesParent.removeChild( savedLineNode ); savedLineNode.dispose(); savedLineNode = null; } if ( savedQuadratic ) { savedLineNode = new QuadraticNode( new Property( savedQuadratic ), model.graph.xRange, model.graph.yRange, model.modelViewTransform, viewProperties.equationForm, viewProperties.equationsVisibleProperty, { lineWidth: GQConstants.SAVED_QUADRATIC_LINE_WIDTH, preventVertexAndEquationOverlap: options.preventVertexAndEquationOverlap } ); // Add it in the foreground, so the user can see it. // See https://github.com/phetsims/graphing-quadratics/issues/36 allLinesParent.addChild( savedLineNode ); } } ); // When quadraticProperty changes, move saved line to background. // See https://github.com/phetsims/graphing-quadratics/issues/36 model.quadraticProperty.link( quadratic => { savedLineNode && savedLineNode.moveToBack(); } ); // Show/hide the graph contents viewProperties.graphContentsVisibleProperty.link( visible => { contentParent.visible = visible; if ( !visible ) { decorationsLayer.interruptSubtreeInput(); // cancel interaction with graph contents } } ); }
/** * RadioButtonGroup constructor. * * @param {Property} property * @param {Array} contentArray an array of objects that have two keys each: value and node the node key holds a * scenery Node that is the content for a given radio button. the value key should hold the value that the property * takes on for the corresponding node to be selected. Optionally, these objects can have an attribute 'label', which * is a {Node} used to label the button. * @param {Object} [options] * @constructor */ function RadioButtonGroup( property, contentArray, options ) { options = options || {}; assert && assert( !options.hasOwnProperty( 'children' ), 'Cannot pass in children to a RadioButtonGroup, ' + 'create siblings in the parent node instead' ); // make sure every object in the content array has properties 'node' and 'value' assert && assert( _.every( contentArray, function( obj ) { return obj.hasOwnProperty( 'node' ) && obj.hasOwnProperty( 'value' ); } ), 'contentArray must be an array of objects with properties "node" and "value"' ); var i; // for loops // make sure that each value passed into the contentArray is unique var uniqueValues = []; for ( i = 0; i < contentArray.length; i++ ) { if ( uniqueValues.indexOf( contentArray[ i ].value ) < 0 ) { uniqueValues.push( contentArray[ i ].value ); } else { throw new Error( 'Duplicate value: "' + contentArray[ i ].value + '" passed into RadioButtonGroup.js' ); } } // make sure that the property passed in currently has a value from the contentArray if ( uniqueValues.indexOf( property.get() ) === -1 ) { throw new Error( 'The property passed in to RadioButtonGroup has an illegal value "' + property.get() + '" that is not present in the contentArray' ); } var defaultOptions = { // LayoutBox options (super class of RadioButtonGroup) spacing: 10, orientation: 'vertical', align: 'center', enabledProperty: new Property( true ), // whether or not the set of radio buttons as a whole is enabled // The fill for the rectangle behind the radio buttons. Default color is bluish color, as in the other button library. baseColor: ColorConstants.LIGHT_BLUE, disabledBaseColor: ColorConstants.LIGHT_GRAY, // Opacity can be set separately for the buttons and button content. selectedButtonOpacity: 1, deselectedButtonOpacity: 0.6, selectedContentOpacity: 1, deselectedContentOpacity: 0.6, overButtonOpacity: 0.8, overContentOpacity: 0.8, selectedStroke: 'black', deselectedStroke: new Color( 50, 50, 50 ), selectedLineWidth: 1.5, deselectedLineWidth: 1, // The following options specify highlight behavior overrides, leave as null to get the default behavior // Note that highlighting applies only to deselected buttons overFill: null, overStroke: null, overLineWidth: null, // These margins are *within* each button buttonContentXMargin: 5, buttonContentYMargin: 5, // TouchArea expansion touchAreaXDilation: 0, touchAreaYDilation: 0, // MouseArea expansion mouseAreaXDilation: 0, mouseAreaYDilation: 0, //The radius for each button cornerRadius: 4, // How far from the button the text label is (only applies if labels are passed in) labelSpacing: 0, // Which side of the button the label will appear, options are 'top', 'bottom', 'left', 'right' // (only applies if labels are passed in) labelAlign: 'bottom', // The default appearances use the color values specified above, but other appearances could be specified for more // customized behavior. Generally setting the color values above should be enough to specify the desired look. buttonAppearanceStrategy: RadioButtonGroupAppearance.defaultRadioButtonsAppearance, contentAppearanceStrategy: RadioButtonGroupAppearance.contentAppearanceStrategy, // optional accessibility description, which applies to the entire group of buttons accessibleLegendDescription: '' }; options = _.extend( _.clone( defaultOptions ), options ); // make a copy of the options to pass to individual buttons that includes all default options but not scenery options var buttonOptions = _.pick( options, _.keys( defaultOptions ) ); // calculate the maximum width and height of the content so we can make all radio buttons the same size var maxWidth = _.max( contentArray, function( content ) { return content.node.width; } ).node.width; var maxHeight = _.max( contentArray, function( content ) { return content.node.height; } ).node.height; // make sure all radio buttons are the same size and create the RadioButtons var buttons = []; var button; for ( i = 0; i < contentArray.length; i++ ) { // each individual radio button will have a different margin to make sure they are all the same size var xMargin = ( ( maxWidth - contentArray[ i ].node.width ) / 2 ) + options.buttonContentXMargin; var yMargin = ( ( maxHeight - contentArray[ i ].node.height ) / 2 ) + options.buttonContentYMargin; var radioButton = new RadioButtonGroupMember( property, contentArray[ i ].value, _.extend( { content: contentArray[ i ].node, xMargin: xMargin, yMargin: yMargin, tandem: contentArray[ i ].tandem, accessibleLabel: contentArray[ i ].accessibleLabel }, buttonOptions ) ); // ensure the buttons don't resize when selected vs unselected by adding a rectangle with the max size var maxLineWidth = Math.max( options.selectedLineWidth, options.deselectedLineWidth ); var maxButtonWidth = maxLineWidth + contentArray[ i ].node.width + xMargin * 2; var maxButtonHeight = maxLineWidth + contentArray[ i ].node.height + yMargin * 2; var boundingRect = new Rectangle( 0, 0, maxButtonWidth, maxButtonHeight, { fill: 'rgba(0,0,0,0)', center: radioButton.center } ); radioButton.addChild( boundingRect ); // if a label is given, the button becomes a LayoutBox with the label and button if ( contentArray[ i ].label ) { var label = contentArray[ i ].label; var labelOrientation = ( options.labelAlign === 'bottom' || options.labelAlign === 'top' ) ? 'vertical' : 'horizontal'; var labelChildren = ( options.labelAlign === 'left' || options.labelAlign === 'top' ) ? [ label, radioButton ] : [ radioButton, label ]; button = new LayoutBox( { children: labelChildren, spacing: options.labelSpacing, orientation: labelOrientation } ); var xDilation = options.touchAreaXDilation; var yDilation = options.touchAreaYDilation; // override the touch and mouse areas defined in RectangularButtonView // extra width is added to the SingleRadioButtons so they don't change size if the line width changes, // that is why lineWidth is subtracted from the width and height when calculating these new areas radioButton.touchArea = Shape.rectangle( -xDilation, -yDilation, button.width + 2 * xDilation - maxLineWidth, button.height + 2 * yDilation - maxLineWidth ); xDilation = options.mouseAreaXDilation; yDilation = options.mouseAreaYDilation; radioButton.mouseArea = Shape.rectangle( -xDilation, -yDilation, button.width + 2 * xDilation - maxLineWidth, button.height + 2 * yDilation - maxLineWidth ); // make sure the label mouse and touch areas don't block the expanded button touch and mouse areas label.pickable = false; // use the same content appearance strategy for the labels that is used for the button content options.contentAppearanceStrategy( label, radioButton.interactionStateProperty, options ); } else { button = radioButton; } buttons.push( button ); } // @private this.enabledProperty = options.enabledProperty; // super call options.children = buttons; LayoutBox.call( this, options ); var thisNode = this; // When the entire RadioButtonGroup gets disabled, gray them out and make them unpickable (and vice versa) this.enabledProperty.link( function( isEnabled ) { thisNode.pickable = isEnabled; for ( i = 0; i < contentArray.length; i++ ) { if ( buttons[ i ] instanceof LayoutBox ) { for ( var j = 0; j < 2; j++ ) { buttons[ i ].children[ j ].enabled = isEnabled; } } else { buttons[ i ].enabled = isEnabled; } } } ); // make the unselected buttons pickable and have a pointer cursor property.link( function( value ) { if ( thisNode.enabledProperty.get() ) { for ( i = 0; i < contentArray.length; i++ ) { if ( contentArray[ i ].value === value ) { buttons[ i ].pickable = false; buttons[ i ].cursor = null; } else { buttons[ i ].pickable = true; buttons[ i ].cursor = 'pointer'; } } } } ); // generate accessible peer for the parallel DOM this.accessibleContent = { createPeer: function( accessibleInstance ) { var trail = accessibleInstance.trail; var uniqueId = trail.getUniqueId(); /* We want the element of the parallel DOM to look like <fieldset id="radio-button-group" role="radiogroup" aria-describedby="legend-id group-description"> <legend>Translatable legend text</legend> ... (radio inputs defined in RadioButtonGroupMember) <p id="group-description">Translatable description of the entire group.</p> </fieldset> */ // create the fieldset holding all radio buttons var domElement = document.createElement( 'fieldset' ); domElement.id = 'radio-button-group-' + uniqueId; domElement.setAttribute( 'role', 'radiogroup' ); var legendElement = document.createElement( 'legend' ); legendElement.innerHTML = options.accessibleLegendDescription; domElement.appendChild( legendElement ); return new AccessiblePeer( accessibleInstance, domElement ); } }; }
/** * @param {Node[]} items - items in the carousel * @param {Object} [options] * @constructor */ function Carousel( items, options ) { var self = this; // Override defaults with specified options options = _.extend( {}, DEFAULT_OPTIONS, options ); // Validate options assert && assert( _.includes( [ 'horizontal', 'vertical' ], options.orientation ), 'invalid orientation=' + options.orientation ); Tandem.indicateUninstrumentedCode(); // To improve readability var isHorizontal = ( options.orientation === 'horizontal' ); // Dimensions of largest item var maxItemWidth = _.maxBy( items, function( item ) { return item.width; } ).width; var maxItemHeight = _.maxBy( items, function( item ) { return item.height; } ).height; // This quantity is used make some other computations independent of orientation. var maxItemLength = isHorizontal ? maxItemWidth : maxItemHeight; // Options common to both buttons var buttonOptions = { xMargin: 5, yMargin: 5, cornerRadius: options.cornerRadius, baseColor: options.buttonColor, disabledBaseColor: options.fill, // same as carousel background stroke: options.buttonStroke, lineWidth: options.buttonLineWidth, minWidth: isHorizontal ? 0 : maxItemWidth + ( 2 * options.margin ), // fill the width of a vertical carousel minHeight: isHorizontal ? maxItemHeight + ( 2 * options.margin ) : 0, // fill the height of a horizontal carousel arrowSize: options.arrowSize, arrowStroke: options.arrowStroke, arrowLineWidth: options.arrowLineWidth, touchAreaXDilation: options.buttonTouchAreaXDilation, touchAreaYDilation: options.buttonTouchAreaYDilation, mouseAreaXDilation: options.buttonMouseAreaXDilation, mouseAreaYDilation: options.buttonMouseAreaYDilation }; // Next/previous buttons var nextButton = new CarouselButton( _.extend( { arrowDirection: isHorizontal ? 'right' : 'down' }, buttonOptions ) ); var previousButton = new CarouselButton( _.extend( { arrowDirection: isHorizontal ? 'left' : 'up' }, buttonOptions ) ); // Computations related to layout of items var numberOfSeparators = ( options.separatorsVisible ) ? ( items.length - 1 ) : 0; var scrollingLength = ( items.length * ( maxItemLength + options.spacing ) + ( numberOfSeparators * options.spacing ) + options.spacing ); var scrollingWidth = isHorizontal ? scrollingLength : ( maxItemWidth + 2 * options.margin ); var scrollingHeight = isHorizontal ? ( maxItemHeight + 2 * options.margin ) : scrollingLength; var itemCenter = options.spacing + ( maxItemLength / 2 ); // Options common to all separators var separatorOptions = { stroke: options.separatorColor, lineWidth: options.separatorLineWidth }; // @private enables animation when scrolling between pages this._animationEnabled = options.animationEnabled; // All items, arranged in the proper orientation, with margins and spacing. // Horizontal carousel arrange items left-to-right, vertical is top-to-bottom. // Translation of this node will be animated to give the effect of scrolling through the items. var scrollingNode = new Rectangle( 0, 0, scrollingWidth, scrollingHeight ); items.forEach( function( item ) { // add the item if ( isHorizontal ) { item.centerX = itemCenter; item.centerY = options.margin + ( maxItemHeight / 2 ); } else { item.centerX = options.margin + ( maxItemWidth / 2 ); item.centerY = itemCenter; } scrollingNode.addChild( item ); // center for the next item itemCenter += ( options.spacing + maxItemLength ); // add optional separator if ( options.separatorsVisible ) { var separator; if ( isHorizontal ) { // vertical separator, to the left of the item separator = new VSeparator( scrollingHeight, _.extend( { centerX: item.centerX + ( maxItemLength / 2 ) + options.spacing, centerY: item.centerY }, separatorOptions ) ); scrollingNode.addChild( separator ); // center for the next item itemCenter = separator.centerX + options.spacing + ( maxItemLength / 2 ); } else { // horizontal separator, below the item separator = new HSeparator( scrollingWidth, _.extend( { centerX: item.centerX, centerY: item.centerY + ( maxItemLength / 2 ) + options.spacing }, separatorOptions ) ); scrollingNode.addChild( separator ); // center for the next item itemCenter = separator.centerY + options.spacing + ( maxItemLength / 2 ); } } } ); // How much to translate scrollingNode each time a next/previous button is pressed var scrollingDelta = options.itemsPerPage * ( maxItemLength + options.spacing ); if ( options.separatorsVisible ) { scrollingDelta += ( options.itemsPerPage * options.spacing ); } // Clipping window, to show one page at a time. // Clips at the midpoint of spacing between items so that you don't see any stray bits of the items that shouldn't be visible. var windowLength = ( scrollingDelta + options.spacing ); if ( options.separatorsVisible ) { windowLength -= options.spacing; } var windowWidth = isHorizontal ? windowLength : scrollingNode.width; var windowHeight = isHorizontal ? scrollingNode.height : windowLength; var clipArea = isHorizontal ? Shape.rectangle( options.spacing / 2, 0, windowWidth - options.spacing, windowHeight ) : Shape.rectangle( 0, options.spacing / 2, windowWidth, windowHeight - options.spacing ); var windowNode = new Node( { children: [ scrollingNode ], clipArea: clipArea } ); // Background - displays the carousel's fill color var backgroundWidth = isHorizontal ? ( windowWidth + nextButton.width + previousButton.width ) : windowWidth; var backgroundHeight = isHorizontal ? windowHeight : ( windowHeight + nextButton.height + previousButton.height ); var backgroundNode = new Rectangle( 0, 0, backgroundWidth, backgroundHeight, options.cornerRadius, options.cornerRadius, { fill: options.fill } ); // Foreground - displays the carousel's outline, created as a separate node so that it can be placed on top of everything, for a clean look. var foregroundNode = new Rectangle( 0, 0, backgroundWidth, backgroundHeight, options.cornerRadius, options.cornerRadius, { stroke: options.stroke } ); // Layout if ( isHorizontal ) { nextButton.centerY = previousButton.centerY = windowNode.centerY = backgroundNode.centerY; nextButton.right = backgroundNode.right; previousButton.left = backgroundNode.left; windowNode.centerX = backgroundNode.centerX; } else { nextButton.centerX = previousButton.centerX = windowNode.centerX = backgroundNode.centerX; nextButton.bottom = backgroundNode.bottom; previousButton.top = backgroundNode.top; windowNode.centerY = backgroundNode.centerY; } // Number of pages var numberOfPages = items.length / options.itemsPerPage; if ( !Util.isInteger( numberOfPages ) ) { numberOfPages = Math.floor( numberOfPages + 1 ); } // Number of the page that is visible in the carousel. assert && assert( options.defaultPageNumber >= 0 && options.defaultPageNumber <= numberOfPages - 1, 'defaultPageNumber is out of range: ' + options.defaultPageNumber ); var pageNumberProperty = new Property( options.defaultPageNumber ); // Change pages var scrollAnimation = null; function pageNumberListener( pageNumber ) { assert && assert( pageNumber >= 0 && pageNumber <= numberOfPages - 1, 'pageNumber out of range: ' + pageNumber ); // button state nextButton.enabled = pageNumber < ( numberOfPages - 1 ); previousButton.enabled = pageNumber > 0; if ( options.hideDisabledButtons ) { nextButton.visible = nextButton.enabled; previousButton.visible = previousButton.enabled; } // stop any animation that's in progress scrollAnimation && scrollAnimation.stop(); if ( self._animationEnabled ) { // options that are independent of orientation var animationOptions = { duration: options.animationDuration, stepEmitter: options.stepEmitter, easing: Easing.CUBIC_IN_OUT }; // options that are specific to orientation if ( isHorizontal ) { animationOptions = _.extend( { getValue: function() { return scrollingNode.left; }, setValue: function( value ) { scrollingNode.left = value; }, to: -pageNumber * scrollingDelta }, animationOptions ); } else { animationOptions = _.extend( { getValue: function() { return scrollingNode.top; }, setValue: function( value ) { scrollingNode.top = value; }, to: -pageNumber * scrollingDelta }, animationOptions ); } // create and start the scroll animation scrollAnimation = new Animation( animationOptions ); scrollAnimation.start(); } else { // animation disabled, move immediate to new page if ( isHorizontal ) { scrollingNode.left = -pageNumber * scrollingDelta; } else { scrollingNode.top = -pageNumber * scrollingDelta; } } } pageNumberProperty.link( pageNumberListener ); // Buttons modify the page number nextButton.addListener( function() { pageNumberProperty.set( pageNumberProperty.get() + 1 ); } ); previousButton.addListener( function() { pageNumberProperty.set( pageNumberProperty.get() - 1 ); } ); // fields this.items = items; // @private this.itemsPerPage = options.itemsPerPage; // @private this.numberOfPages = numberOfPages; // @public (read-only) {number} number of pages in the carousel this.pageNumberProperty = pageNumberProperty; // @public {Property<number>} page number that is currently visible options.children = [ backgroundNode, windowNode, nextButton, previousButton, foregroundNode ]; this.disposeCarousel = function() { pageNumberProperty.unlink( pageNumberListener ); }; Node.call( this, options ); }