diff --git a/js/common/NumberPlayConstants.js b/js/common/NumberPlayConstants.js index 3baf667d..4bf36197 100644 --- a/js/common/NumberPlayConstants.js +++ b/js/common/NumberPlayConstants.js @@ -70,6 +70,7 @@ define( require => { ORANGE_BACKGROUND: 'rgb( 255, 218, 176 )', PURPLE_BACKGROUND: 'rgb( 254, 202, 255 )', BLUE_BACKGROUND: 'rgb( 190, 232, 255 )', + WHITE_BACKGROUND: 'rgb( 255, 255, 255 )', BUCKET_BASE_COLOR: 'rgb( 100, 101, 162 )', // misc TODO: when base classes exist, move bucket specs there diff --git a/js/compare/CompareScreen.js b/js/compare/CompareScreen.js index 708432f9..5c5af081 100644 --- a/js/compare/CompareScreen.js +++ b/js/compare/CompareScreen.js @@ -13,8 +13,10 @@ define( require => { const CompareScreenView = require( 'NUMBER_PLAY/compare/view/CompareScreenView' ); const Image = require( 'SCENERY/nodes/Image' ); const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const NumberPlayConstants = require( 'NUMBER_PLAY/common/NumberPlayConstants' ); const Property = require( 'AXON/Property' ); const Screen = require( 'JOIST/Screen' ); + const Vector2 = require( 'DOT/Vector2' ); // strings const screenCompareString = require( 'string!NUMBER_PLAY/screen.compare' ); @@ -31,13 +33,17 @@ define( require => { const options = { name: screenCompareString, - backgroundColorProperty: new Property( 'white' ), + backgroundColorProperty: new Property( NumberPlayConstants.WHITE_BACKGROUND ), homeScreenIcon: new Image( compareScreenIconImage ), tandem: tandem }; super( - () => new CompareModel( tandem.createTandem( 'model' ) ), + () => new CompareModel( + NumberPlayConstants.TWENTY, + new Vector2( 16, 262 ), // empirically determined + 1.3, // empirically determined + tandem.createTandem( 'model' ) ), model => new CompareScreenView( model, tandem.createTandem( 'view' ) ), options ); diff --git a/js/compare/model/CompareModel.js b/js/compare/model/CompareModel.js index 25e685b3..41f91a02 100644 --- a/js/compare/model/CompareModel.js +++ b/js/compare/model/CompareModel.js @@ -9,32 +9,59 @@ define( require => { 'use strict'; // modules + const BooleanProperty = require( 'AXON/BooleanProperty' ); const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const ComparePlayArea = require( 'NUMBER_PLAY/compare/model/ComparePlayArea' ); + const NumberProperty = require( 'AXON/NumberProperty' ); + const Range = require( 'DOT/Range' ); - class CompareModel { + class CompareModel { /** + * @param {number} highestCount - the highest integer number that can be counted to + * @param {Vector2} paperNumberOrigin - see OnesPlayArea for doc + * @param {number} objectMaxScale - see PlayObject for doc * @param {Tandem} tandem */ - constructor( tandem ) { - //TODO + constructor( highestCount, paperNumberOrigin, objectMaxScale, tandem ) { + + // @public {NumberProperty} + this.leftCurrentNumberProperty = new NumberProperty( 0, { + range: new Range( 0, highestCount ) + } ); + this.rightCurrentNumberProperty = new NumberProperty( 0, { + range: new Range( 0, highestCount ) + } ); + + // @public {BooleanProperty} - see NumberPlayModel for doc + this.isResettingProperty = new BooleanProperty( false ); + + // @public + this.leftPlayArea = new ComparePlayArea( this.leftCurrentNumberProperty, objectMaxScale, paperNumberOrigin, this.isResettingProperty ); + this.rightPlayArea = new ComparePlayArea( this.rightCurrentNumberProperty, objectMaxScale, paperNumberOrigin, this.isResettingProperty ); } /** - * Resets the model. + * Steps the model. + * @param {number} dt - time step, in seconds * @public */ - reset() { - //TODO + step( dt ) { + this.leftPlayArea.step( dt ); + this.rightPlayArea.step( dt ); } /** - * Steps the model. - * @param {number} dt - time step, in seconds + * Resets the model. * @public */ - step( dt ) { - //TODO + reset() { + this.isResettingProperty.value = true; + this.leftPlayArea.reset(); + this.rightPlayArea.reset(); + this.leftCurrentNumberProperty.reset(); + this.rightCurrentNumberProperty.reset(); + this.isResettingProperty.reset(); } } diff --git a/js/compare/model/ComparePlayArea.js b/js/compare/model/ComparePlayArea.js new file mode 100644 index 00000000..1a968023 --- /dev/null +++ b/js/compare/model/ComparePlayArea.js @@ -0,0 +1,72 @@ +// Copyright 2019, University of Colorado Boulder + +/** + * Model class for a ComparePlayArea, which combines a OnesPlayArea and an ObjectPlayArea. + * + * @author Chris Klusendorf (PhET Interactive Simulations) + */ +define( require => { + 'use strict'; + + // modules + const EnumerationProperty = require( 'AXON/EnumerationProperty' ); + const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const ObjectsPlayArea = require( 'NUMBER_PLAY/common/model/ObjectsPlayArea' ); + const OnesPlayArea = require( 'NUMBER_PLAY/common/model/OnesPlayArea' ); + const ComparePlayObjectType = require( 'NUMBER_PLAY/compare/model/ComparePlayObjectType' ); + + class ComparePlayArea { + + /** + * @param {NumberProperty} currentNumberProperty + * @param {number} objectMaxScale - see PlayObject for doc + * @param {Vector2} paperNumberOrigin - see OnesPlayArea for doc + * @param {BooleanProperty} isResetting + */ + constructor( currentNumberProperty, objectMaxScale, paperNumberOrigin, isResettingProperty ) { + + // @public {EnumerationProperty.} - the current type of playObject being displayed + this.playObjectTypeProperty = new EnumerationProperty( ComparePlayObjectType, ComparePlayObjectType.DOG ); + + // since one value of ComparePlayObjectType is not valid in ObjectsPlayArea, this is a separate Property + // to prevent that value from passing through to ObjectsPlayArea. see the link below for usage. + const playObjectTypeProperty = new EnumerationProperty( ComparePlayObjectType, ComparePlayObjectType.DOG ); + + // @public (read-only) - the model for managing paper ones in the playArea + this.onesPlayArea = new OnesPlayArea( currentNumberProperty, paperNumberOrigin, isResettingProperty ); + + // @public (read-only) - the model for managing objects in the playArea + this.objectsPlayArea = new ObjectsPlayArea( currentNumberProperty, objectMaxScale, isResettingProperty, { + playObjectTypeProperty: playObjectTypeProperty + } ); + + // if the value of the current play object type is a paper one, don't send update the Property that was passed to + // ObjectsPlayArea, as it does not handle paper ones. instead, see how this same link is used in + // CompareAccordionBox to hide ObjectsPlayArea and show the OnesPlayArea for this case. + this.playObjectTypeProperty.link( type => { + if ( type !== ComparePlayObjectType.PAPER_ONE ) { + playObjectTypeProperty.value = type; + } + } ); + } + + /** + * @param {number} dt - time step, in seconds + * @public + */ + step( dt ) { + this.onesPlayArea.step( dt ); + } + + /** + * @public + */ + reset() { + this.onesPlayArea.reset(); + this.objectsPlayArea.reset(); + this.playObjectTypeProperty.reset(); + } + } + + return numberPlay.register( 'ComparePlayArea', ComparePlayArea ); +} ); \ No newline at end of file diff --git a/js/compare/model/ComparePlayObjectType.js b/js/compare/model/ComparePlayObjectType.js new file mode 100644 index 00000000..4176c37c --- /dev/null +++ b/js/compare/model/ComparePlayObjectType.js @@ -0,0 +1,20 @@ +// Copyright 2019, University of Colorado Boulder + +/** + * Play object types specific to the `Compare` screen. + * + * @author Chris Klusendorf + */ +define( require => { + 'use strict'; + + // modules + const Enumeration = require( 'PHET_CORE/Enumeration' ); + const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const PlayObjectType = require( 'NUMBER_PLAY/common/model/PlayObjectType' ); + + // @public + const ComparePlayObjectType = new Enumeration( [ PlayObjectType.DOG.name, PlayObjectType.APPLE.name, 'PAPER_ONE' ] ); + + return numberPlay.register( 'ComparePlayObjectType', ComparePlayObjectType ); +} ); \ No newline at end of file diff --git a/js/compare/view/CompareAccordionBox.js b/js/compare/view/CompareAccordionBox.js new file mode 100644 index 00000000..a79788d3 --- /dev/null +++ b/js/compare/view/CompareAccordionBox.js @@ -0,0 +1,162 @@ +// Copyright 2019, University of Colorado Boulder + +/** + * Class for the `Objects` accordion box on the 'Compare' screen, which mixes the functionality of ObjectsAccordionBox + * and OnesAccordionBox + * + * TODO: Generalize the ObjectsAccordionBox and OnesAccordionBox so that they share code, which will remove the need + * to use both ObjectsPlayAreaNode and OnesPlayAreaNode. + * + * @author Chris Klusendorf (PhET Interactive Simulations) + */ +define( require => { + 'use strict'; + + // modules + const AccordionBox = require( 'SUN/AccordionBox' ); + const BaseNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/BaseNumber' ); + const BaseNumberNode = require( 'MAKE_A_TEN/make-a-ten/common/view/BaseNumberNode' ); + const Bounds2 = require( 'DOT/Bounds2' ); + const ComparePlayObjectType = require( 'NUMBER_PLAY/compare/model/ComparePlayObjectType' ); + const Color = require( 'SCENERY/util/Color' ); + const Dimension2 = require( 'DOT/Dimension2' ); + const EnumerationProperty = require( 'AXON/EnumerationProperty' ); + const merge = require( 'PHET_CORE/merge' ); + const ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' ); + const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const NumberPlayConstants = require( 'NUMBER_PLAY/common/NumberPlayConstants' ); + const ObjectsPlayAreaNode = require( 'NUMBER_PLAY/common/view/ObjectsPlayAreaNode' ); + const OnesPlayAreaNode = require( 'NUMBER_PLAY/common/view/OnesPlayAreaNode' ); + const PlayObject = require( 'NUMBER_PLAY/common/model/PlayObject' ); + const PlayObjectNode = require( 'NUMBER_PLAY/common/view/PlayObjectNode' ); + const RadioButtonGroup = require( 'SUN/buttons/RadioButtonGroup' ); + const Rectangle = require( 'SCENERY/nodes/Rectangle' ); + const Text = require( 'SCENERY/nodes/Text' ); + const Vector2 = require( 'DOT/Vector2' ); + + // constants + const WIDTH = 394; // the width of this AccordionBox, in screen coordinates. from the screen's design asset. + + // strings + const objectsString = require( 'string!NUMBER_PLAY/objects' ); + + class CompareAccordionBox extends AccordionBox { + + /** + * @param {ComparePlayArea} playArea + * @param {number} height - the height of this accordion box + * @param {Object} [options] + */ + constructor( playArea, height, options ) { + + options = merge( { + titleNode: new Text( objectsString, { font: NumberPlayConstants.ACCORDION_BOX_TITLE_FONT } ), + fill: NumberPlayConstants.BLUE_BACKGROUND, + minWidth: WIDTH, + maxWidth: WIDTH, + + contentWidth: 350, // {number} + radioButtonSize: new Dimension2( 28, 28 ), // {Dimension2} + radioButtonSpacing: 10 // {number} + }, NumberPlayConstants.ACCORDION_BOX_OPTIONS, options ); + + const contentNode = new Rectangle( { + rectHeight: height, + rectWidth: options.contentWidth + } ); + + // create view bounds for the ObjectsPlayAreaNode + const playAreaMarginY = 15; + const objectsPlayAreaViewBounds = new Bounds2( + contentNode.left, + contentNode.top + playAreaMarginY, + contentNode.right, + contentNode.bottom - playAreaMarginY + ); + const translateMVT = ModelViewTransform2.createSinglePointScaleInvertedYMapping( + Vector2.ZERO, + new Vector2( objectsPlayAreaViewBounds.left + NumberPlayConstants.BUCKET_SIZE.width / 2, objectsPlayAreaViewBounds.bottom ), + 1 + ); + const playAreaModelBounds = translateMVT.viewToModelBounds( objectsPlayAreaViewBounds ).dilatedX( -20 ).dilatedY( -19 ); + + // create and add the ObjectsPlayAreaNode + const objectsPlayAreaNode = new ObjectsPlayAreaNode( + playArea.objectsPlayArea, + playAreaModelBounds, + translateMVT + ); + contentNode.addChild( objectsPlayAreaNode ); + + // create view bounds for the OnesPlayAreaNode + const onesPlayAreaViewBounds = new Bounds2( + contentNode.left, + contentNode.top, + contentNode.right, + contentNode.bottom - playAreaMarginY + ); + + // create and add the OnesPlayAreaNode + const onesPlayAreaNode = new OnesPlayAreaNode( + playArea.onesPlayArea, + onesPlayAreaViewBounds, + translateMVT + ); + contentNode.addChild( onesPlayAreaNode ); + + // create the icons for the RadioButtonGroup + const buttons = []; + ComparePlayObjectType.VALUES.forEach( playObjectType => { + let iconNode = null; + if ( playObjectType === ComparePlayObjectType.PAPER_ONE ) { + iconNode = new BaseNumberNode( new BaseNumber( 1, 0 ), 1 ); + iconNode.setScaleMagnitude( options.radioButtonSize.height / iconNode.height / 4 ); + } + else { + iconNode = new PlayObjectNode( + new PlayObject( + new EnumerationProperty( ComparePlayObjectType, playObjectType ), + new Vector2( 0, 0 ), + options.radioButtonSize, + 1 + ), + playAreaModelBounds, + translateMVT + ); + } + + buttons.push( { + value: playObjectType, + node: iconNode + } ); + } ); + + // create and add the RadioButtonGroup, which is a control for changing the ComparePlayObjectType of this play area + const radioButtonGroup = new RadioButtonGroup( playArea.playObjectTypeProperty, buttons, { + baseColor: Color.WHITE, + orientation: 'horizontal', + spacing: options.radioButtonSpacing + } ); + radioButtonGroup.right = objectsPlayAreaViewBounds.right - 2; // empirically determined tweak + radioButtonGroup.bottom = objectsPlayAreaViewBounds.bottom; + contentNode.addChild( radioButtonGroup ); + + // since (for now) there are two underlying play areas in place of one, hide and show whichever is appropriate + // based on the value of playObjectTypeProperty + playArea.playObjectTypeProperty.link( type => { + if ( type === ComparePlayObjectType.PAPER_ONE ) { + objectsPlayAreaNode.visible = false; + onesPlayAreaNode.visible = true; + } + else { + onesPlayAreaNode.visible = false; + objectsPlayAreaNode.visible = true; + } + } ); + + super( contentNode, options ); + } + } + + return numberPlay.register( 'CompareAccordionBox', CompareAccordionBox ); +} ); \ No newline at end of file diff --git a/js/compare/view/CompareScreenView.js b/js/compare/view/CompareScreenView.js index 6468eaaa..f67451e3 100644 --- a/js/compare/view/CompareScreenView.js +++ b/js/compare/view/CompareScreenView.js @@ -9,12 +9,19 @@ define( require => { 'use strict'; // modules - const Image = require( 'SCENERY/nodes/Image' ); + const BooleanProperty = require( 'AXON/BooleanProperty' ); + const CompareAccordionBox = require( 'NUMBER_PLAY/compare/view/CompareAccordionBox' ); + const merge = require( 'PHET_CORE/merge' ); const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const NumberPlayConstants = require( 'NUMBER_PLAY/common/NumberPlayConstants' ); + const NumeralAccordionBox = require( 'NUMBER_PLAY/common/view/NumeralAccordionBox' ); + const PhetFont = require( 'SCENERY_PHET/PhetFont' ); + const ResetAllButton = require( 'SCENERY_PHET/buttons/ResetAllButton' ); const ScreenView = require( 'JOIST/ScreenView' ); - // images - const compareScreenPlaceholderImage = require( 'image!NUMBER_PLAY/compare_screen_placeholder.png' ); + // constants + const UPPER_ACCORDION_BOX_HEIGHT = 120; // empirically determined, in screen coordinates + const LOWER_ACCORDION_BOX_HEIGHT = 426; // empirically determined, in screen coordinates class CompareScreenView extends ScreenView { @@ -28,14 +35,73 @@ define( require => { tandem: tandem } ); - // add the screen's placeholder image - const compareScreenPlaceholderImageNode = new Image( compareScreenPlaceholderImage, { - maxWidth: this.layoutBounds.width, - maxHeight: this.layoutBounds.height, - cursor: 'pointer' + // @public + this.leftNumeralAccordionBoxExpandedProperty = new BooleanProperty( true ); + this.rightNumeralAccordionBoxExpandedProperty = new BooleanProperty( true ); + this.leftCompareAccordionBoxExpandedProperty = new BooleanProperty( true ); + this.rightCompareAccordionBoxExpandedProperty = new BooleanProperty( true ); + + // config for the left and right NumeralAccordionBox + const numeralAccordionBoxConfig = { + fill: NumberPlayConstants.WHITE_BACKGROUND, + font: new PhetFont( 90 ), + arrowButtonConfig: { + arrowWidth: 18, // empirically determined + arrowHeight: 18, // empirically determined + spacing: 7 // empirically determined + } + }; + + // create and add the left NumeralAccordionBox + const leftNumeralAccordionBox = new NumeralAccordionBox( + model.leftCurrentNumberProperty, + UPPER_ACCORDION_BOX_HEIGHT, merge( { + expandedProperty: this.leftNumeralAccordionBoxExpandedProperty + }, numeralAccordionBoxConfig ) ); + leftNumeralAccordionBox.top = this.layoutBounds.minY + NumberPlayConstants.ACCORDION_BOX_TOP_MARGIN; + this.addChild( leftNumeralAccordionBox ); + + // create and add the right NumeralAccordionBox + const rightNumeralAccordionBox = new NumeralAccordionBox( + model.rightCurrentNumberProperty, + UPPER_ACCORDION_BOX_HEIGHT, merge( { + expandedProperty: this.rightNumeralAccordionBoxExpandedProperty + }, numeralAccordionBoxConfig ) ); + rightNumeralAccordionBox.top = leftNumeralAccordionBox.top; + this.addChild( rightNumeralAccordionBox ); + + // create and add the left CompareAccordionBox + const leftCompareAccordionBox = new CompareAccordionBox( model.leftPlayArea, LOWER_ACCORDION_BOX_HEIGHT, { + expandedProperty: this.leftCompareAccordionBoxExpandedProperty + } ); + leftCompareAccordionBox.left = this.layoutBounds.minX + NumberPlayConstants.ACCORDION_BOX_X_MARGIN; + leftCompareAccordionBox.bottom = this.layoutBounds.maxY - NumberPlayConstants.ACCORDION_BOX_BOTTOM_MARGIN; + this.addChild( leftCompareAccordionBox ); + + // create and add the right CompareAccordionBox + const rightCompareAccordionBox = new CompareAccordionBox( model.rightPlayArea, LOWER_ACCORDION_BOX_HEIGHT, { + expandedProperty: this.rightCompareAccordionBoxExpandedProperty + } ); + rightCompareAccordionBox.right = this.layoutBounds.maxX - NumberPlayConstants.ACCORDION_BOX_X_MARGIN; + rightCompareAccordionBox.bottom = leftCompareAccordionBox.bottom; + this.addChild( rightCompareAccordionBox ); + + // set the x-position of the NumeralAccordionBox's after the CompareObjectAccordionBoxes have been added + leftNumeralAccordionBox.right = leftCompareAccordionBox.right; + rightNumeralAccordionBox.left = rightCompareAccordionBox.left; + + // create and add the ResetAllButton + const resetAllButton = new ResetAllButton( { + listener: () => { + this.interruptSubtreeInput(); // cancel interactions that may be in progress + model.reset(); + this.reset(); + }, + right: this.layoutBounds.maxX - NumberPlayConstants.SCREEN_VIEW_X_PADDING, + bottom: this.layoutBounds.maxY - NumberPlayConstants.SCREEN_VIEW_Y_PADDING, + tandem: tandem.createTandem( 'resetAllButton' ) } ); - compareScreenPlaceholderImageNode.center = this.layoutBounds.center; - this.addChild( compareScreenPlaceholderImageNode ); + this.addChild( resetAllButton ); } /** @@ -43,7 +109,10 @@ define( require => { * @public */ reset() { - //TODO + this.leftNumeralAccordionBoxExpandedProperty.reset(); + this.rightNumeralAccordionBoxExpandedProperty.reset(); + this.leftCompareAccordionBoxExpandedProperty.reset(); + this.rightCompareAccordionBoxExpandedProperty.reset(); } }