define( function( require ) { 'use strict'; // modules var inherit = require( 'PHET_CORE/inherit' ); var makeATen = require( 'MAKE_A_TEN/makeATen' ); /** * @constructor * * @param {number} leftTerm * @param {number} rightTerm */ function NumberChallenge( leftTerm, rightTerm ) { // @public {number} - The left-hand term for addition this.leftTerm = leftTerm; // @public {number} - The right-hand term for addition this.rightTerm = rightTerm; // This object is immutable Object.freeze( this ); } makeATen.register( 'NumberChallenge', NumberChallenge ); return inherit( Object, NumberChallenge ); } );
define( function( require ) { 'use strict'; // modules var inherit = require( 'PHET_CORE/inherit' ); var makeATen = require( 'MAKE_A_TEN/makeATen' ); var MakeATenAddingModel = require( 'MAKE_A_TEN/make-a-ten/adding/model/MakeATenAddingModel' ); var MakeATenAddingScreenView = require( 'MAKE_A_TEN/make-a-ten/adding/view/MakeATenAddingScreenView' ); var MakeATenConstants = require( 'MAKE_A_TEN/make-a-ten/common/MakeATenConstants' ); var MakeATenUtil = require( 'MAKE_A_TEN/make-a-ten/common/MakeATenUtil' ); var Property = require( 'AXON/Property' ); var Screen = require( 'JOIST/Screen' ); // images var addingHomeScreenImage = require( 'image!MAKE_A_TEN/adding-home-screen.png' ); var addingNavBarImage = require( 'image!MAKE_A_TEN/adding-nav-bar.png' ); // strings var screenAddingString = require( 'string!MAKE_A_TEN/screen.adding' ); /** * @constructor */ function MakeATenAddingScreen() { var options = { name: screenAddingString, backgroundColorProperty: new Property( MakeATenConstants.SCREEN_BACKGROUND_COLOR ), homeScreenIcon: MakeATenUtil.createIconWithBackgroundColor( addingHomeScreenImage, MakeATenConstants.SCREEN_BACKGROUND_COLOR ), navigationBarIcon: MakeATenUtil.createIconWithBackgroundColor( addingNavBarImage, MakeATenConstants.SCREEN_BACKGROUND_COLOR ) }; Screen.call( this, function() { return new MakeATenAddingModel(); }, function( model ) { return new MakeATenAddingScreenView( model ); }, options ); } makeATen.register( 'MakeATenAddingScreen', MakeATenAddingScreen ); return inherit( Screen, MakeATenAddingScreen ); } );
define( function( require ) { 'use strict'; // modules var makeATen = require( 'MAKE_A_TEN/makeATen' ); var MakeATenQueryParameters = QueryStringMachine.getAll( { // Initializes the Explore screen with specific numbers, spaced horizontally, // e.g. ?exploreNumbers=10,51, where 0 indicates none. exploreNumbers: { type: 'array', elementSchema: { type: 'number' }, defaultValue: [ 10 ] } } ); makeATen.register( 'MakeATenQueryParameters', MakeATenQueryParameters ); return MakeATenQueryParameters; } );
define( function( require ) { 'use strict'; // modules var ArithmeticRules = require( 'MAKE_A_TEN/make-a-ten/common/model/ArithmeticRules' ); var arrayRemove = require( 'PHET_CORE/arrayRemove' ); var BaseNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/BaseNumber' ); var BaseNumberNode = require( 'MAKE_A_TEN/make-a-ten/common/view/BaseNumberNode' ); var DragListener = require( 'SCENERY/listeners/DragListener' ); var Emitter = require( 'AXON/Emitter' ); var inherit = require( 'PHET_CORE/inherit' ); var makeATen = require( 'MAKE_A_TEN/makeATen' ); var Node = require( 'SCENERY/nodes/Node' ); var PaperNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/PaperNumber' ); var Rectangle = require( 'SCENERY/nodes/Rectangle' ); /** * @constructor * * @param {PaperNumber} paperNumber * @param {Property.<Bounds2>} availableViewBoundsProperty * @param {Function} addAndDragNumber - function( event, paperNumber ), adds and starts a drag for a number * @param {Function} tryToCombineNumbers - function( paperNumber ), called to combine our paper number */ function PaperNumberNode( paperNumber, availableViewBoundsProperty, addAndDragNumber, tryToCombineNumbers ) { var self = this; Node.call( this ); // @public {PaperNumber} - Our model this.paperNumber = paperNumber; // @public {Emitter} - Triggered with self when this paper number node starts to get dragged this.moveEmitter = new Emitter( { validators: [ { valueType: PaperNumberNode } ] } ); // @public {Emitter} - Triggered with self when this paper number node is split this.splitEmitter = new Emitter( { validators: [ { valueType: PaperNumberNode } ] } ); // @public {Emitter} - Triggered when user interaction with this paper number begins. this.interactionStartedEmitter = new Emitter( { validators: [ { valueType: PaperNumberNode } ] } ); // @private {boolean} - When true, don't emit from the moveEmitter (synthetic drag) this.preventMoveEmit = false; // @private {Bounds2} this.availableViewBoundsProperty = availableViewBoundsProperty; // @private {Node} - Container for the digit image nodes this.numberImageContainer = new Node( { pickable: false } ); this.addChild( this.numberImageContainer ); // @private {Rectangle} - Hit target for the "split" behavior, where one number would be pulled off from the // existing number. this.splitTarget = new Rectangle( 0, 0, 100, 100, { cursor: 'pointer' } ); this.addChild( this.splitTarget ); // @private {Rectangle} - Hit target for the "move" behavior, which just drags the existing paper number. this.moveTarget = new Rectangle( 0, 0, 100, 100, { cursor: 'move' } ); this.addChild( this.moveTarget ); // View-coordinate offset between our position and the pointer's position, used for keeping drags synced. // @private {DragListener} this.moveDragHandler = new DragListener( { targetNode: this, start: function( event, listener ) { self.interactionStartedEmitter.emit( self ); if ( !self.preventMoveEmit ) { self.moveEmitter.emit( self ); } }, drag: function( event, listener ) { paperNumber.setConstrainedDestination( availableViewBoundsProperty.value, listener.parentPoint ); }, end: function( event, listener ) { tryToCombineNumbers( self.paperNumber ); paperNumber.endDragEmitter.emit( paperNumber ); } } ); this.moveDragHandler.isUserControlledProperty.link( function( controlled ) { paperNumber.userControlledProperty.value = controlled; } ); this.moveTarget.addInputListener( this.moveDragHandler ); // @private {Object} this.splitDragHandler = { down: function( event ) { if ( !event.canStartPress() ) { return; } var viewPosition = self.globalToParentPoint( event.pointer.point ); // Determine how much (if any) gets moved off var pulledPlace = paperNumber.getBaseNumberAt( self.parentToLocalPoint( viewPosition ) ).place; var amountToRemove = ArithmeticRules.pullApartNumbers( paperNumber.numberValueProperty.value, pulledPlace ); var amountRemaining = paperNumber.numberValueProperty.value - amountToRemove; // it cannot be split - so start moving if ( !amountToRemove ) { self.startSyntheticDrag( event ); return; } paperNumber.changeNumber( amountRemaining ); self.interactionStartedEmitter.emit( self ); self.splitEmitter.emit( self ); var newPaperNumber = new PaperNumber( amountToRemove, paperNumber.positionProperty.value ); addAndDragNumber( event, newPaperNumber ); } }; this.splitTarget.addInputListener( this.splitDragHandler ); // @private {Function} - Listener that hooks model position to view translation. this.translationListener = function( position ) { self.translation = position; }; // @private {Function} - Listener for when our number changes this.updateNumberListener = this.updateNumber.bind( this ); // @private {Function} - Listener reference that gets attached/detached. Handles moving the Node to the front. this.userControlledListener = function( userControlled ) { if ( userControlled ) { self.moveToFront(); } }; } makeATen.register( 'PaperNumberNode', PaperNumberNode ); return inherit( Node, PaperNumberNode, { /** * Rebuilds the image nodes that display the actual paper number, and resizes the mouse/touch targets. * @private */ updateNumber: function() { var self = this; var reversedBaseNumbers = this.paperNumber.baseNumbers.slice().reverse(); // Reversing allows easier opacity computation and has the nodes in order for setting children. this.numberImageContainer.children = _.map( reversedBaseNumbers, function( baseNumber, index ) { // each number has successively less opacity on top return new BaseNumberNode( baseNumber, 0.95 * Math.pow( 0.97, index ) ); } ); // Grab the bounds of the biggest base number for the full bounds var fullBounds = this.paperNumber.baseNumbers[ this.paperNumber.baseNumbers.length - 1 ].bounds; // Split target only visible if our number is > 1. Move target can resize as needed. if ( this.paperNumber.numberValueProperty.value === 1 ) { self.splitTarget.visible = false; self.moveTarget.mouseArea = self.moveTarget.touchArea = self.moveTarget.rectBounds = fullBounds; } else { self.splitTarget.visible = true; // Locate the boundary between the "move" input area and "split" input area. var boundaryY = this.paperNumber.getBoundaryY(); // Modify our move/split targets self.moveTarget.mouseArea = self.moveTarget.touchArea = self.moveTarget.rectBounds = fullBounds.withMinY( boundaryY ); self.splitTarget.mouseArea = self.splitTarget.touchArea = self.splitTarget.rectBounds = fullBounds.withMaxY( boundaryY ); } // Changing the number must have happened from an interaction. If combined, we want to put cues on this. this.interactionStartedEmitter.emit( this ); }, /** * Called when we grab an event from a different input (like clicking the paper number in the explore panel, or * splitting paper numbers), and starts a drag on this paper number. * @public * * @param {Event} event - Scenery event from the relevant input handler */ startSyntheticDrag: function( event ) { // Don't emit a move event, as we don't want the cue to disappear. this.preventMoveEmit = true; this.moveDragHandler.press( event ); this.preventMoveEmit = false; }, /** * Implements the API for ClosestDragListener. * @public * * @param {Event} event - Scenery event from the relevant input handler */ startDrag: function( event ) { if ( this.globalToLocalPoint( event.pointer.point ).y < this.splitTarget.bottom && this.paperNumber.numberValueProperty.value > 1 ) { this.splitDragHandler.down( event ); } else { this.moveDragHandler.press( event ); } }, /** * Implements the API for ClosestDragListener. * @public * * @param {Vector2} globalPoint */ computeDistance: function( globalPoint ) { if ( this.paperNumber.userControlledProperty.value ) { return Number.POSITIVE_INFINITY; } else { var globalBounds = this.localToGlobalBounds( this.paperNumber.getLocalBounds() ); return Math.sqrt( globalBounds.minimumDistanceToPointSquared( globalPoint ) ); } }, /** * Attaches listeners to the model. Should be called when added to the scene graph. * @public */ attachListeners: function() { // mirrored unlinks in detachListeners() this.paperNumber.userControlledProperty.link( this.userControlledListener ); this.paperNumber.numberValueProperty.link( this.updateNumberListener ); this.paperNumber.positionProperty.link( this.translationListener ); }, /** * Removes listeners from the model. Should be called when removed from the scene graph. * @public */ detachListeners: function() { this.paperNumber.positionProperty.unlink( this.translationListener ); this.paperNumber.numberValueProperty.unlink( this.updateNumberListener ); this.paperNumber.userControlledProperty.unlink( this.userControlledListener ); }, /** * Find all nodes which are attachable to the dragged node. This method is called once the user ends the dragging. * @public * * @param {Array.<PaperNumberNode>} allPaperNumberNodes * @returns {Array} */ findAttachableNodes: function( allPaperNumberNodes ) { var self = this; var attachableNodeCandidates = allPaperNumberNodes.slice(); arrayRemove( attachableNodeCandidates, this ); return attachableNodeCandidates.filter( function( candidateNode ) { return PaperNumber.arePaperNumbersAttachable( self.paperNumber, candidateNode.paperNumber ); } ); } }, { /** * Given a number's digit and place, looks up the associated image. * @public * * @param {number} digit * @param {number} place * @returns {HTMLImageElement} */ getNumberImage: function( digit, place ) { return new BaseNumberNode( new BaseNumber( digit, place ), 1 ); } } ); } );
define( function( require ) { 'use strict'; // modules var ActiveTerm = require( 'MAKE_A_TEN/make-a-ten/adding/model/ActiveTerm' ); var HBox = require( 'SCENERY/nodes/HBox' ); var inherit = require( 'PHET_CORE/inherit' ); var makeATen = require( 'MAKE_A_TEN/makeATen' ); var MakeATenConstants = require( 'MAKE_A_TEN/make-a-ten/common/MakeATenConstants' ); var MathSymbols = require( 'SCENERY_PHET/MathSymbols' ); var Node = require( 'SCENERY/nodes/Node' ); var PhetFont = require( 'SCENERY_PHET/PhetFont' ); var Property = require( 'AXON/Property' ); var Rectangle = require( 'SCENERY/nodes/Rectangle' ); var Text = require( 'SCENERY/nodes/Text' ); // constants var EQUATION_FONT = new PhetFont( { size: 45, weight: 'bold' } ); var STROKE_COLOR = '#000'; var LAYOUT_MULTIPLIER = 1 / 8; // Fraction offset of the text from the background's border /** * @constructor * * @param {AdditionTerms} additionTerms - Our model, contains information about the left/right and active terms * @param {boolean} highlightBorders - Whether there should be highlighted borders around the active term. */ function AdditionTermsNode( additionTerms, highlightBorders ) { Node.call( this ); var backgroundOptions = { stroke: STROKE_COLOR, lineDash: [ 5, 5 ], lineWidth: 2, visible: highlightBorders }; var leftNumberDisplayBackground = new Rectangle( 0, 0, 100, 78, 10, 10, backgroundOptions ); var rightNumberDisplayBackground = new Rectangle( 0, 0, 100, 78, 10, 10, backgroundOptions ); var plusText = new Text( MathSymbols.PLUS, { font: EQUATION_FONT, fill: MakeATenConstants.EQUATION_FILL } ); var equalsSignText = new Text( MathSymbols.EQUAL_TO, { font: EQUATION_FONT, fill: MakeATenConstants.EQUATION_FILL } ); var numberDisplayBox = new HBox( { children: [ leftNumberDisplayBackground, plusText, rightNumberDisplayBackground ], spacing: 5, resize: false // since we toggle the stroke } ); var leftTermText = new Text( '', { font: EQUATION_FONT, fill: MakeATenConstants.EQUATION_FILL } ); var rightTermText = new Text( '', { font: EQUATION_FONT, fill: MakeATenConstants.EQUATION_FILL } ); this.addChild( numberDisplayBox ); this.addChild( leftTermText ); this.addChild( rightTermText ); this.addChild( equalsSignText ); function layout() { if ( !rightTermText.bounds.isEmpty() ) { equalsSignText.left = rightTermText.right + 20; } if ( !leftTermText.bounds.isEmpty() ) { leftTermText.right = leftNumberDisplayBackground.right - leftNumberDisplayBackground.width * LAYOUT_MULTIPLIER; } } additionTerms.leftTermProperty.link( function( term ) { leftTermText.text = term ? term : ''; layout(); } ); additionTerms.rightTermProperty.link( function( term ) { rightTermText.text = term ? term : ''; layout(); } ); // Add highlights if applicable if ( highlightBorders ) { Property.multilink( [ additionTerms.leftTermProperty, additionTerms.activeTermProperty ], function( leftTerm, activeTerm ) { leftNumberDisplayBackground.stroke = ( leftTerm === 0 || activeTerm === ActiveTerm.LEFT ) ? STROKE_COLOR : null; leftNumberDisplayBackground.fill = activeTerm === ActiveTerm.LEFT ? 'white' : null; } ); Property.multilink( [ additionTerms.rightTermProperty, additionTerms.activeTermProperty ], function( rightTerm, activeTerm ) { rightNumberDisplayBackground.stroke = ( rightTerm === 0 || activeTerm === ActiveTerm.RIGHT ) ? STROKE_COLOR : null; rightNumberDisplayBackground.fill = activeTerm === ActiveTerm.RIGHT ? 'white' : null; } ); Property.multilink( [ additionTerms.leftTermProperty, additionTerms.rightTermProperty, additionTerms.activeTermProperty ], function() { equalsSignText.visible = additionTerms.hasBothTerms(); } ); } // Center everything vertically var centerY = numberDisplayBox.centerY; leftTermText.centerY = centerY; rightTermText.centerY = centerY; equalsSignText.centerY = centerY; // Unchanging layout position of the right text node rightTermText.left = rightNumberDisplayBackground.left + rightNumberDisplayBackground.width * LAYOUT_MULTIPLIER; // @public this.getLeftAlignment = function() { return leftTermText.right; }; this.getRightAlignment = function() { return rightTermText.left; }; } makeATen.register( 'AdditionTermsNode', AdditionTermsNode ); return inherit( Node, AdditionTermsNode ); } );
define( function( require ) { 'use strict'; // modules var ArrowNode = require( 'SCENERY_PHET/ArrowNode' ); var Color = require( 'SCENERY/util/Color' ); var Image = require( 'SCENERY/nodes/Image' ); var inherit = require( 'PHET_CORE/inherit' ); var makeATen = require( 'MAKE_A_TEN/makeATen' ); var MakeATenConstants = require( 'MAKE_A_TEN/make-a-ten/common/MakeATenConstants' ); var Node = require( 'SCENERY/nodes/Node' ); var Rectangle = require( 'SCENERY/nodes/Rectangle' ); // images var handImage = require( 'image!SCENERY_PHET/hand.png' ); /** * @constructor * * @param {Cue} cue - Our cue model */ function SplitCueNode( cue ) { Node.call( this, { pickable: false, usesOpacity: true } ); // @private {Cue} this.cue = cue; var arrowOptions = { fill: MakeATenConstants.CUE_FILL, stroke: null, headHeight: 14, headWidth: 22, tailWidth: 9, x: 7, y: 3 }; this.seeThroughRectangle = new Rectangle( 0, 0, 100, 100, { fill: new Color( MakeATenConstants.CUE_FILL ).withAlpha( 0.2 ) } ); this.addChild( this.seeThroughRectangle ); this.arrowContainer = new Node( { children: [ new ArrowNode( 0, 0, 30, -30, arrowOptions ), new Image( handImage, { scale: 0.3, rotation: Math.PI / 6 - Math.PI / 5 } ) ] } ); this.addChild( this.arrowContainer ); var updatePositionListener = this.updatePosition.bind( this ); var updateRectangleListener = this.updateRectangle.bind( this ); cue.visibilityProperty.linkAttribute( this, 'visible' ); cue.opacityProperty.linkAttribute( this, 'opacity' ); cue.visibilityProperty.link( updatePositionListener ); // update position when we become visible cue.paperNumberProperty.link( function( newPaperNumber, oldPaperNumber ) { if ( newPaperNumber ) { newPaperNumber.positionProperty.link( updatePositionListener ); // translation newPaperNumber.numberValueProperty.link( updatePositionListener ); // may have changed bounds newPaperNumber.numberValueProperty.link( updateRectangleListener ); // may have changed bounds } if ( oldPaperNumber ) { oldPaperNumber.numberValueProperty.unlink( updateRectangleListener ); oldPaperNumber.numberValueProperty.unlink( updatePositionListener ); oldPaperNumber.positionProperty.unlink( updatePositionListener ); } } ); } makeATen.register( 'SplitCueNode', SplitCueNode ); return inherit( Node, SplitCueNode, { /** * Updates the position of the cue. * @private */ updatePosition: function() { var visible = this.cue.visibilityProperty.value; var paperNumber = this.cue.paperNumberProperty.value; if ( visible && paperNumber ) { var position = paperNumber.positionProperty.value; var localBounds = paperNumber.getLocalBounds(); this.setTranslation( position ); this.arrowContainer.setTranslation( localBounds.right - 22, localBounds.top + 15 ); } }, /** * Updates the size of the semi-transparent rectangle. * @private */ updateRectangle: function() { var paperNumber = this.cue.paperNumberProperty.value; if ( paperNumber ) { var bounds = paperNumber.getLocalBounds(); var boundaryY = paperNumber.getBoundaryY(); this.seeThroughRectangle.setRectBounds( bounds.withMaxY( boundaryY ) ); } } } ); } );
define( function( require ) { 'use strict'; // modules var BaseNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/BaseNumber' ); var BooleanProperty = require( 'AXON/BooleanProperty' ); var Bounds2 = require( 'DOT/Bounds2' ); var Emitter = require( 'AXON/Emitter' ); var inherit = require( 'PHET_CORE/inherit' ); var makeATen = require( 'MAKE_A_TEN/makeATen' ); var MakeATenConstants = require( 'MAKE_A_TEN/make-a-ten/common/MakeATenConstants' ); var MakeATenUtil = require( 'MAKE_A_TEN/make-a-ten/common/MakeATenUtil' ); var NumberProperty = require( 'AXON/NumberProperty' ); var Vector2 = require( 'DOT/Vector2' ); var Vector2Property = require( 'DOT/Vector2Property' ); // Incremented for PaperNumber IDs var nextPaperNumberId = 1; /** * @constructor * * @param {number} numberValue - Numeric value, e.g. 123 * @param {Vector2} initialPosition */ function PaperNumber( numberValue, initialPosition ) { // @public {number} - IDs required for map-like lookup, see https://github.com/phetsims/make-a-ten/issues/199 this.id = nextPaperNumberId++; // @public {NumberProperty} - The number that this model represents, e.g. 324 this.numberValueProperty = new NumberProperty( numberValue ); // @public Property that indicates where in model space the upper left corner of this shape is. In general, this // should not be set directly outside of this type, and should be manipulated through the methods defined below. this.positionProperty = new Vector2Property( initialPosition.copy() ); // @public {BooleanProperty} - Flag that tracks whether the user is dragging this number around. Should be set // externally, generally by the view node. this.userControlledProperty = new BooleanProperty( false ); // @public {Vector2} - Destination is used for animation, and should be set through accessor methods only. this.destination = initialPosition.copy(); // @private // @public {boolean} - Whether this element is animating from one location to another, do not set externally. this.animating = false; // @public {Array.<BaseNumber>} - Represents the non-zero place values in this number. 1034 will have three place // values, 4, 30 and 1000, which when summed will equal our number. this.baseNumbers = PaperNumber.getBaseNumbers( this.numberValueProperty.value ); // @public {Emitter} - Fires when the user stops dragging a paper number. this.endDragEmitter = new Emitter( { validators: [ { valueType: PaperNumber } ] } ); // @public {Emitter} - Fires when the animation towards our destination ends (we hit our destination). this.endAnimationEmitter = new Emitter( { validators: [ { valueType: PaperNumber } ] } ); } makeATen.register( 'PaperNumber', PaperNumber ); return inherit( Object, PaperNumber, { /** * Animates the number towards its destination. * @public * * @param {number} dt */ step: function( dt ) { if ( !this.userControlledProperty.value ) { var currentPosition = this.positionProperty.value; assert && assert( currentPosition.isFinite() ); assert && assert( this.destination.isFinite() ); // perform any animation var distanceToDestination = currentPosition.distance( this.destination ); if ( distanceToDestination > dt * MakeATenConstants.ANIMATION_VELOCITY ) { // Move a step toward the destination. var stepVector = this.destination.minus( currentPosition ).setMagnitude( MakeATenConstants.ANIMATION_VELOCITY * dt ); assert && assert( stepVector.isFinite() ); this.positionProperty.value = currentPosition.plus( stepVector ); } else if ( this.animating ) { // Less than one time step away, so just go to the destination. this.positionProperty.value = this.destination; this.animating = false; this.endAnimationEmitter.emit( this ); } } }, /** * The number of digits in the number, including zeros, e.g. 1204 has 4 digits. * @public * * @returns {number} */ get digitLength() { assert && assert( this.numberValueProperty.value > 0 ); return MakeATenUtil.digitsInNumber( this.numberValueProperty.value ); }, /** * Returns the bounds of the paper number relative to the paper number's origin. * @public * * @returns {Bounds2} */ getLocalBounds: function() { // Use the largest base number return this.baseNumbers[ this.baseNumbers.length - 1 ].bounds; }, /** * Locate the boundary between the "move" input area and "split" input area, in the number's local bounds. * @public * * @returns {Bounds2} */ getBoundaryY: function() { var bounds = this.getLocalBounds(); var moveToSplitRatio = MakeATenConstants.SPLIT_BOUNDARY_HEIGHT_PROPORTION; return bounds.maxY * ( 1 - moveToSplitRatio ) + bounds.minY * moveToSplitRatio; }, /** * Returns the ideal spot to "drag" a number from (near the center of its move target) relative to its origin. * @public * * @returns {Vector2} */ getDragTargetOffset: function() { var bounds = this.getLocalBounds(); var ratio = MakeATenConstants.SPLIT_BOUNDARY_HEIGHT_PROPORTION / 2; return new Vector2( bounds.centerX, bounds.minY * ratio + bounds.maxY * ( 1 - ratio ) ); }, /** * Changes the number that this paper number represents. * @public * * @param {number} numberValue */ changeNumber: function( numberValue ) { assert && assert( typeof numberValue === 'number' ); this.baseNumbers = PaperNumber.getBaseNumbers( numberValue ); this.numberValueProperty.value = numberValue; }, /** * Sets the destination of the number. If animate is false, it also sets the position. * @public * * @param {Vector2} destination * @param {boolean} animate - Whether to animate. If true, it will slide towards the destination. If false, it will * immediately set the position to be the same as the destination. */ setDestination: function( destination, animate ) { assert && assert( destination.isFinite() ); this.destination = destination; if ( animate ) { this.animating = true; } else { this.positionProperty.value = destination; } }, /** * If our paper number is outside of the available view bounds, move in inside those bounds. * @public * * @param {Bounds2} viewBounds * @param {Vector2} position * @param {boolean} [animate] - Indicates if the new constrained position should be directly set or animated */ setConstrainedDestination: function( viewBounds, newDestination, animate ) { // Determine how our number's origin can be placed in the bounds var localBounds = this.getLocalBounds(); var padding = 10; var originBounds = new Bounds2( viewBounds.left - localBounds.left, viewBounds.top - localBounds.top, viewBounds.right - localBounds.right, viewBounds.bottom - localBounds.bottom ).eroded( padding ); this.setDestination( originBounds.closestPointTo( newDestination ), animate ); }, /** * Returns the lowest place number whose bounds include the position. * @public * * @param {Vector2} position - Position relative to this number's origin. * @returns {BaseNumber} */ getBaseNumberAt: function( position ) { for ( var i = 0; i < this.baseNumbers.length; i++ ) { assert && assert( i === 0 || this.baseNumbers[ i ].place > this.baseNumbers[ i - 1 ].place, 'Ensure that we start at lower places, required for this to work properly' ); var baseNumber = this.baseNumbers[ i ]; if ( baseNumber.bounds.containsPoint( position ) ) { return baseNumber; } } // Outside of the bounds, so we need to check each and determine the closest. for ( i = 0; i < this.baseNumbers.length; i++ ) { baseNumber = this.baseNumbers[ i ]; if ( position.x > baseNumber.bounds.left ) { return baseNumber; } } // Default the largest one. return this.baseNumbers[ this.baseNumbers.length - 1 ]; } }, { /** * Given a number, returns an array of BaseNumbers that will represent the digit places. * @public * * @param {number} number - The number we want to break into digit places. * @returns {Array.<BaseNumber>} */ getBaseNumbers: function( number ) { assert && assert( number > 0 && number % 1 === 0 ); var result = []; // Divide by 10 each loop, using the remainder and place location to create the place numbers. var remainder = number; var place = 0; while ( remainder ) { var digit = remainder % 10; if ( digit ) { result.push( new BaseNumber( digit, place ) ); } remainder = ( remainder - digit ) / 10; place++; } return result; }, /** * Returns whether the two paper numbers are close enough to be "attached" to each other. * @public * * @param {PaperNumber} paperNumber1 * @param {PaperNumber} paperNumber2 * @returns {boolean} */ arePaperNumbersAttachable: function( paperNumber1, paperNumber2 ) { var firstLarger = paperNumber1.numberValueProperty.value > paperNumber2.numberValueProperty.value; var largePaperNumber = firstLarger ? paperNumber1 : paperNumber2; var smallPaperNumber = firstLarger ? paperNumber2 : paperNumber1; var smallCenter = smallPaperNumber.positionProperty.value.plus( smallPaperNumber.getLocalBounds().center ); var largePosition = largePaperNumber.positionProperty.value; var largeBounds = largePaperNumber.getLocalBounds().shifted( largePosition.x, largePosition.y ); var unitX = ( smallCenter.x - largeBounds.centerX ) / ( largeBounds.width / 2 ); var unitY = ( smallCenter.y - largeBounds.centerY ) / ( largeBounds.height / 2 ); return unitX * unitX + 2 * unitY * unitY < 1; } } ); } );
define( function( require ) { 'use strict'; // modules var inherit = require( 'PHET_CORE/inherit' ); var makeATen = require( 'MAKE_A_TEN/makeATen' ); var MakeATenConstants = require( 'MAKE_A_TEN/make-a-ten/common/MakeATenConstants' ); var ObservableArray = require( 'AXON/ObservableArray' ); var PaperNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/PaperNumber' ); var Vector2 = require( 'DOT/Vector2' ); /** * @constructor */ function MakeATenCommonModel() { // @public {ObservableArray.<PaperNumber>} - Numbers in play that can be interacted with. this.paperNumbers = new ObservableArray(); } makeATen.register( 'MakeATenCommonModel', MakeATenCommonModel ); return inherit( Object, MakeATenCommonModel, { /** * Steps the model forward by a unit of time. * * @param {number} dt */ step: function( dt ) { // Cap large dt values, which can occur when the tab containing // the sim had been hidden and then re-shown dt = Math.min( 0.1, dt ); for ( var i = 0; i < this.paperNumbers.length; i++ ) { this.paperNumbers.get( i ).step( dt ); } }, /** * Given two paper numbers, combine them (set one's value to the sum of their previous values, and remove the * other). * * @param {Bounds2} availableModelBounds - Constrain the location to be inside these bounds * @param {PaperNumber} draggedPaperNumber * @param {PaperNumber} dropTargetNumber */ collapseNumberModels: function( availableModelBounds, draggedPaperNumber, dropTargetNumber ) { var dropTargetNumberValue = dropTargetNumber.numberValueProperty.value; var draggedNumberValue = draggedPaperNumber.numberValueProperty.value; var newValue = dropTargetNumberValue + draggedNumberValue; var numberToRemove; var numberToChange; // See https://github.com/phetsims/make-a-ten/issues/260 if ( draggedPaperNumber.digitLength === dropTargetNumber.digitLength ) { numberToRemove = draggedPaperNumber; numberToChange = dropTargetNumber; } else { // The larger number gets changed, the smaller one gets removed. var droppingOnLarger = dropTargetNumberValue > draggedNumberValue; numberToRemove = droppingOnLarger ? draggedPaperNumber : dropTargetNumber; numberToChange = droppingOnLarger ? dropTargetNumber : draggedPaperNumber; } // Apply changes this.removePaperNumber( numberToRemove ); numberToChange.changeNumber( newValue ); numberToChange.setConstrainedDestination( availableModelBounds, numberToChange.positionProperty.value, false ); }, /** * Add a PaperNumber to the model * @public * * @param {PaperNumber} paperNumber */ addPaperNumber: function( paperNumber ) { this.paperNumbers.push( paperNumber ); }, /** * Remove a PaperNumber from the model * @public * * @param {PaperNumber} paperNumber */ removePaperNumber: function( paperNumber ) { this.paperNumbers.remove( paperNumber ); }, /** * Remove all PaperNumbers from the model. * @public * * @param {PaperNumber} paperNumber */ removeAllPaperNumbers: function() { this.paperNumbers.clear(); }, /** * Given an array of integers, create and add paper numbers for each that are evenly distributed across the screen. * @public * * @param {Array.<number>} numbers */ addMultipleNumbers: function( numbers ) { for ( var i = 0; i < numbers.length; i++ ) { var number = numbers[ i ]; // Ingore 0s if ( !number ) { continue; } // evenly distribute across the screen var x = MakeATenConstants.LAYOUT_BOUNDS.width * ( 1 + i ) / ( numbers.length + 1 ); var initialNumberPosition = new Vector2( x, MakeATenConstants.LAYOUT_BOUNDS.height / 2.5 ); var paperNumber = new PaperNumber( number, initialNumberPosition ); this.addPaperNumber( paperNumber ); } }, /** * @param {Bounds2} availableModelBounds - Constrain the location to be inside these bounds * @param {PaperNumber} paperNumber1 * @param {PaperNumber} paperNumber2 */ repelAway: function( availableModelBounds, paperNumber1, paperNumber2 ) { // Determine which are 'left' and 'right' var isPaper1Left = paperNumber1.positionProperty.value.x < paperNumber2.positionProperty.value.x; var leftPaperNumber = isPaper1Left ? paperNumber1 : paperNumber2; var rightPaperNumber = isPaper1Left ? paperNumber2 : paperNumber1; // Determine offsets var repelLeftOffset = -MakeATenConstants.MOVE_AWAY_DISTANCE[ leftPaperNumber.digitLength ]; var repelRightOffset = MakeATenConstants.MOVE_AWAY_DISTANCE[ rightPaperNumber.digitLength ]; var leftPosition = leftPaperNumber.positionProperty.value.plusXY( repelLeftOffset, 0 ); var rightPosition = rightPaperNumber.positionProperty.value.plusXY( repelRightOffset, 0 ); // Kick off the animation to the destination var animateToDestination = true; leftPaperNumber.setConstrainedDestination( availableModelBounds, leftPosition, animateToDestination ); rightPaperNumber.setConstrainedDestination( availableModelBounds, rightPosition, animateToDestination ); }, /** * Reset the model * @public */ reset: function() { this.removeAllPaperNumbers(); } } ); } );
define( function( require ) { 'use strict'; // modules var BooleanProperty = require( 'AXON/BooleanProperty' ); var Checkbox = require( 'SUN/Checkbox' ); var ExplorePanel = require( 'MAKE_A_TEN/make-a-ten/explore/view/ExplorePanel' ); var HBox = require( 'SCENERY/nodes/HBox' ); var inherit = require( 'PHET_CORE/inherit' ); var makeATen = require( 'MAKE_A_TEN/makeATen' ); var MakeATenCommonView = require( 'MAKE_A_TEN/make-a-ten/common/view/MakeATenCommonView' ); var MakeATenConstants = require( 'MAKE_A_TEN/make-a-ten/common/MakeATenConstants' ); var MathSymbols = require( 'SCENERY_PHET/MathSymbols' ); var PaperNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/PaperNumber' ); var PhetFont = require( 'SCENERY_PHET/PhetFont' ); var SplitCueNode = require( 'MAKE_A_TEN/make-a-ten/explore/view/SplitCueNode' ); var Text = require( 'SCENERY/nodes/Text' ); var Vector2 = require( 'DOT/Vector2' ); // strings var hideTotalString = require( 'string!MAKE_A_TEN/hideTotal' ); // constants var EQUATION_FONT = new PhetFont( { size: 60, weight: 'bold' } ); /** * @param {MakeATenExploreModel} model * @constructor */ function MakeATenExploreScreenView( model ) { var self = this; // @private {Function} - Called with function( paperNumberNode ) on number splits this.numberSplitListener = this.onNumberSplit.bind( this ); // @private {Function} - Called with function( paperNumberNode ) when a number begins to be interacted with. this.numberInteractionListener = this.onNumberInteractionStarted.bind( this ); // @private {Function} - Called with function( paperNumber ) when a number finishes animation this.numberAnimationFinishedListener = this.onNumberAnimationFinished.bind( this ); // @private {Function} - Called with function( paperNumber ) when a number finishes being dragged this.numberDragFinishedListener = this.onNumberDragFinished.bind( this ); MakeATenCommonView.call( this, model ); // @private {BooleanProperty} - Whether the total (sum) is hidden this.hideSumProperty = new BooleanProperty( false ); var sumText = new Text( '0', { font: EQUATION_FONT, fill: MakeATenConstants.EQUATION_FILL } ); model.sumProperty.linkAttribute( sumText, 'text' ); // @private {HBox} - Displays the sum of our numbers and an equals sign, e.g. "256 =" this.sumNode = new HBox( { children: [ sumText, new Text( MathSymbols.EQUAL_TO, { font: EQUATION_FONT, fill: MakeATenConstants.EQUATION_FILL } ) ], spacing: 15 } ); this.addChild( this.sumNode ); // @private {ExplorePanel} - Shows 100,10,1 that can be dragged. this.explorePanel = new ExplorePanel( this, model.sumProperty ); this.addChild( this.explorePanel ); var hideSumText = new Text( hideTotalString, { maxWidth: 150, font: new PhetFont( { size: 25, weight: 'bold' } ), fill: 'black' } ); // @private {Checkbox} - When checked, hides the sum in the upper-left this.hideSumCheckbox = new Checkbox( hideSumText, this.hideSumProperty, { spacing: 10, boxWidth: 30 } ); this.hideSumCheckbox.touchArea = this.hideSumCheckbox.localBounds.dilatedXY( 10, 4 ); this.addChild( this.hideSumCheckbox ); this.hideSumProperty.link( function( hideSum ) { self.sumNode.visible = !hideSum; } ); this.addChild( this.paperNumberLayerNode ); this.addChild( new SplitCueNode( model.splitCue ) ); this.layoutControls(); } makeATen.register( 'MakeATenExploreScreenView', MakeATenExploreScreenView ); return inherit( MakeATenCommonView, MakeATenExploreScreenView, { /** * @override */ layoutControls: function() { MakeATenCommonView.prototype.layoutControls.call( this ); var visibleBounds = this.visibleBoundsProperty.value; this.explorePanel.centerX = visibleBounds.centerX; this.explorePanel.bottom = visibleBounds.bottom - 10; this.hideSumCheckbox.left = this.explorePanel.right + 20; this.hideSumCheckbox.bottom = visibleBounds.bottom - 10; this.sumNode.left = visibleBounds.left + 30; this.sumNode.top = visibleBounds.top + 30; }, /** * Whether the paper number is predominantly over the explore panel (should be collected). * @private * * @param {PaperNumber} paperNumber * @returns {boolean} */ isNumberInReturnZone: function( paperNumber ) { // Compute the local point on the number that would need to go into the return zone. // This point is a bit farther down than the exact center, as it was annoying to "miss" the return zone // slightly by being too high (while the mouse WAS in the return zone). var localBounds = paperNumber.getLocalBounds(); var localReturnPoint = localBounds.center.plus( localBounds.centerBottom ).dividedScalar( 2 ); // And the bounds of our panel var panelBounds = this.explorePanel.bounds.withMaxY( this.visibleBoundsProperty.value.bottom ); // View coordinate of our return point var paperCenter = paperNumber.positionProperty.value.plus( localReturnPoint ); return panelBounds.containsPoint( paperCenter ); }, /** * @override */ onPaperNumberAdded: function( paperNumber ) { var paperNumberNode = MakeATenCommonView.prototype.onPaperNumberAdded.call( this, paperNumber ); // Add listeners paperNumberNode.splitEmitter.addListener( this.numberSplitListener ); paperNumberNode.interactionStartedEmitter.addListener( this.numberInteractionListener ); paperNumber.endAnimationEmitter.addListener( this.numberAnimationFinishedListener ); paperNumber.endDragEmitter.addListener( this.numberDragFinishedListener ); }, /** * @override */ onPaperNumberRemoved: function( paperNumber ) { var paperNumberNode = this.findPaperNumberNode( paperNumber ); // Remove listeners paperNumber.endDragEmitter.removeListener( this.numberDragFinishedListener ); paperNumber.endAnimationEmitter.removeListener( this.numberAnimationFinishedListener ); paperNumberNode.interactionStartedEmitter.removeListener( this.numberInteractionListener ); paperNumberNode.splitEmitter.removeListener( this.numberSplitListener ); // Detach any attached cues if ( this.model.splitCue.paperNumberProperty.value === paperNumber ) { this.model.splitCue.detach(); } MakeATenCommonView.prototype.onPaperNumberRemoved.call( this, paperNumber ); }, /** * Called when a paper number node is split. * @private * * @param {PaperNumberNode} paperNumberNode */ onNumberSplit: function( paperNumberNode ) { this.model.splitCue.triggerFade(); }, /** * Called when a paper number node starts being interacted with. * @private * * @param {PaperNumberNode} paperNumberNode */ onNumberInteractionStarted: function( paperNumberNode ) { var paperNumber = paperNumberNode.paperNumber; if ( paperNumber.numberValueProperty.value > 1 ) { this.model.splitCue.attachToNumber( paperNumber ); } }, /** * Called when a paper number has finished animating to its destination. * @private * * @param {PaperNumber} paperNumber */ onNumberAnimationFinished: function( paperNumber ) { // If it animated to the return zone, it's probably split and meant to be returned. if ( this.isNumberInReturnZone( paperNumber ) ) { this.model.removePaperNumber( paperNumber ); } }, /** * Called when a paper number has finished being dragged. * @private * * @param {PaperNumber} paperNumber */ onNumberDragFinished: function( paperNumber ) { // Return it to the panel if it's been dropped in the panel. if ( this.isNumberInReturnZone( paperNumber ) ) { var baseNumbers = paperNumber.baseNumbers; // Split it into a PaperNumber for each of its base numbers, and animate them to their targets in the // explore panel. for ( var i = baseNumbers.length - 1; i >= 0; i-- ) { var baseNumber = baseNumbers[ i ]; var basePaperNumber = new PaperNumber( baseNumber.numberValue, paperNumber.positionProperty.value ); // Set its destination to the proper target (with the offset so that it will disappear once centered). var targetPosition = this.explorePanel.getOriginLocation( baseNumber.digitLength ); var paperCenterOffset = new PaperNumber( baseNumber.numberValue, new Vector2( 0, 0 ) ).getLocalBounds().center; targetPosition = targetPosition.minus( paperCenterOffset ); basePaperNumber.setDestination( targetPosition, true ); // Add the new base paper number this.model.addPaperNumber( basePaperNumber ); } // Remove the original paper number (as we have added its components). this.model.removePaperNumber( paperNumber ); } }, /** * @override */ reset: function() { MakeATenCommonView.prototype.reset.call( this ); this.hideSumProperty.reset(); } } ); } );