diff --git a/images/peeled_paper_background_1.png b/images/peeled_paper_background_1.png new file mode 100644 index 00000000..02c17a68 Binary files /dev/null and b/images/peeled_paper_background_1.png differ diff --git a/images/peeled_paper_background_10.png b/images/peeled_paper_background_10.png new file mode 100644 index 00000000..0c0dd613 Binary files /dev/null and b/images/peeled_paper_background_10.png differ diff --git a/js/common/view/BaseNumberNode.js b/js/common/view/BaseNumberNode.js new file mode 100644 index 00000000..e7122ae9 --- /dev/null +++ b/js/common/view/BaseNumberNode.js @@ -0,0 +1,164 @@ +// Copyright 2016-2019, University of Colorado Boulder + +/** + * Creates image views for base numbers. + * + * @author Jonathan Olson + * @author Chris Klusendorf (PhET Interactive Simulations), copied from make-a-ten and modified for number-play + */ +define( require => { + 'use strict'; + + // modules + const Dimension2 = require( 'DOT/Dimension2' ); + const Image = require( 'SCENERY/nodes/Image' ); + const inherit = require( 'PHET_CORE/inherit' ); + const Node = require( 'SCENERY/nodes/Node' ); + const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const Vector2 = require( 'DOT/Vector2' ); + + // images + const imageDigit0 = require( 'mipmap!MAKE_A_TEN/digit-0.png' ); + const imageDigit1 = require( 'mipmap!MAKE_A_TEN/digit-1.png' ); + const imageDigit2 = require( 'mipmap!MAKE_A_TEN/digit-2.png' ); + const imageDigit3 = require( 'mipmap!MAKE_A_TEN/digit-3.png' ); + const imageDigit4 = require( 'mipmap!MAKE_A_TEN/digit-4.png' ); + const imageDigit5 = require( 'mipmap!MAKE_A_TEN/digit-5.png' ); + const imageDigit6 = require( 'mipmap!MAKE_A_TEN/digit-6.png' ); + const imageDigit7 = require( 'mipmap!MAKE_A_TEN/digit-7.png' ); + const imageDigit8 = require( 'mipmap!MAKE_A_TEN/digit-8.png' ); + const imageDigit9 = require( 'mipmap!MAKE_A_TEN/digit-9.png' ); + const imagePaperBackground1 = require( 'mipmap!MAKE_A_TEN/paper-background-1.png' ); + const imagePaperBackground10 = require( 'mipmap!MAKE_A_TEN/paper-background-10.png' ); + const imagePaperBackground100 = require( 'mipmap!MAKE_A_TEN/paper-background-100.png' ); + const imagePaperBackground1000 = require( 'mipmap!MAKE_A_TEN/paper-background-1000.png' ); + const peeledImagePaperBackground1 = require( 'mipmap!NUMBER_PLAY/peeled_paper_background_1.png' ); + const peeledImagePaperBackground10 = require( 'mipmap!NUMBER_PLAY/peeled_paper_background_10.png' ); + + // place => mipmap info + const PEELED_BACKGROUND_IMAGE_MAP = { + 0: peeledImagePaperBackground1, + 1: peeledImagePaperBackground10 + }; + + // place => mipmap info + const BACKGROUND_IMAGE_MAP = { + 0: imagePaperBackground1, + 1: imagePaperBackground10, + 2: imagePaperBackground100, + 3: imagePaperBackground1000 + }; + + // digit => mipmap info + const DIGIT_IMAGE_MAP = { + 1: imageDigit1, + 2: imageDigit2, + 3: imageDigit3, + 4: imageDigit4, + 5: imageDigit5, + 6: imageDigit6, + 7: imageDigit7, + 8: imageDigit8, + 9: imageDigit9 + }; + + // place => x/y offsets for the first digit in each place + const PLACE_X_OFFSET = { 0: 48, 1: 108, 2: 70, 3: 94 }; + const PLACE_Y_OFFSET = { 0: 65, 1: 85, 2: 163, 3: 197 }; + + // digit => horizontal offset for that digit (applied to all places, includes digit-specific information) + const DIGIT_X_OFFSET = { 1: 93, 2: -7, 3: -7, 4: -9, 5: -18, 6: -5, 7: -24, 8: -2, 9: -10 }; + + // digit => horizontal offset, customized for each single digit base number + const FIRST_PLACE_DIGIT_X_OFFSET = { 1: -61, 2: 0, 3: 0, 4: 0, 5: 5, 6: 0, 7: 15, 8: 10, 9: 15 }; + + // place => horizontal locations of the zeros in the base number + const ZERO_OFFSET = { + 0: [], + 1: [ 335 ], + 2: [ 560, 314 ], + 3: [ 825, 580, 335 ] + }; + + // Scale was increased from 72dpi (pixels) to 300dpi, so that we can have crisper graphics. + const SCALE = 72 / 300; + + /** + * @constructor + * @extends Node + * + * @param {BaseNumber} baseNumber + * @param {number} opacity + * @param {boolean} isPartOfStack - does this baseNumber have other layers to it? + */ + function BaseNumberNode( baseNumber, opacity, isPartOfStack ) { + Node.call( this, { scale: SCALE } ); + + // Location of the initial digit + let x = PLACE_X_OFFSET[ baseNumber.place ] + DIGIT_X_OFFSET[ baseNumber.digit ]; + const y = PLACE_Y_OFFSET[ baseNumber.place ]; + + // We need to slightly offset some + if ( baseNumber.place === 0 ) { + x += FIRST_PLACE_DIGIT_X_OFFSET[ baseNumber.digit ]; + } + + // Translate everything by our offset + this.translation = baseNumber.offset; + + // if the base number is a 1 that's not on top of a bigger base number, or if the base number is underneath smaller + // base numbers, then use a flat background instead of a peeled one. + if ( baseNumber.digit === 1 && + ( ( baseNumber.place === 0 && !isPartOfStack ) || ( baseNumber.place >= 1 && isPartOfStack ) ) ) { + + // The paper behind the numbers + this.addChild( new Image( BACKGROUND_IMAGE_MAP[ baseNumber.place ], { + imageOpacity: opacity + } ) ); + } + else { + this.addChild( new Image( PEELED_BACKGROUND_IMAGE_MAP[ baseNumber.place ], { + imageOpacity: opacity + } ) ); + } + + // The initial (non-zero) digit + this.addChild( new Image( DIGIT_IMAGE_MAP[ baseNumber.digit ], { + x: x, + y: y + } ) ); + + // Add the zeros + const digitZeroOffsets = ZERO_OFFSET[ baseNumber.place ]; + for ( let i = 0; i < digitZeroOffsets.length; i++ ) { + this.addChild( new Image( imageDigit0, { + x: digitZeroOffsets[ i ], + y: y + } ) ); + } + } + + numberPlay.register( 'BaseNumberNode', BaseNumberNode ); + + inherit( Node, BaseNumberNode, {}, { + /** + * @public {Object} - Maps place (0-3) to a {Dimension2} with the width/height + */ + PAPER_NUMBER_DIMENSIONS: _.mapValues( BACKGROUND_IMAGE_MAP, function( mipmap ) { + return new Dimension2( mipmap[ 0 ].width * SCALE, mipmap[ 0 ].height * SCALE ); + } ), + + /** + * @public {Array.} - Maps place (0-3) to a {Vector2} that is the offset of the upper-left corner of the + * BaseNumberNode relative to a 1-digit BaseNumberNode. + */ + IMAGE_OFFSETS: [ + new Vector2( 0, 0 ), + new Vector2( -70, -( PLACE_Y_OFFSET[ 1 ] - PLACE_Y_OFFSET[ 0 ] ) * SCALE ), + new Vector2( -70 - ( ZERO_OFFSET[ 2 ][ 0 ] - ZERO_OFFSET[ 1 ][ 0 ] ) * SCALE, -( PLACE_Y_OFFSET[ 2 ] - PLACE_Y_OFFSET[ 0 ] ) * SCALE ), + new Vector2( -70 - ( ZERO_OFFSET[ 3 ][ 0 ] - ZERO_OFFSET[ 1 ][ 0 ] ) * SCALE, -( PLACE_Y_OFFSET[ 3 ] - PLACE_Y_OFFSET[ 0 ] ) * SCALE ) + ] + } ); + + return BaseNumberNode; +} ); diff --git a/js/common/view/OnesPlayAreaNode.js b/js/common/view/OnesPlayAreaNode.js index 7168f33d..519d1138 100644 --- a/js/common/view/OnesPlayAreaNode.js +++ b/js/common/view/OnesPlayAreaNode.js @@ -18,7 +18,7 @@ define( require => { const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); const OnesCreatorNode = require( 'NUMBER_PLAY/common/view/OnesCreatorNode' ); const PaperNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/PaperNumber' ); - const PaperNumberNode = require( 'MAKE_A_TEN/make-a-ten/common/view/PaperNumberNode' ); + const PaperNumberNode = require( 'NUMBER_PLAY/common/view/PaperNumberNode' ); const Property = require( 'AXON/Property' ); const Rectangle = require( 'SCENERY/nodes/Rectangle' ); const Vector2 = require( 'DOT/Vector2' ); diff --git a/js/common/view/PaperNumberNode.js b/js/common/view/PaperNumberNode.js new file mode 100644 index 00000000..43d4e880 --- /dev/null +++ b/js/common/view/PaperNumberNode.js @@ -0,0 +1,285 @@ +// Copyright 2015-2019, University of Colorado Boulder + +/** + * Visual view of paper numbers (PaperNumber), with stacked images based on the digits of the number. + * + * @author Sharfudeen Ashraf + * @author Chris Klusendorf (PhET Interactive Simulations), copied from make-a-ten + */ +define( require => { + 'use strict'; + + // modules + const ArithmeticRules = require( 'MAKE_A_TEN/make-a-ten/common/model/ArithmeticRules' ); + const arrayRemove = require( 'PHET_CORE/arrayRemove' ); + const BaseNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/BaseNumber' ); + const BaseNumberNode = require( 'NUMBER_PLAY/common/view/BaseNumberNode' ); + const Bounds2 = require( 'DOT/Bounds2' ); + const DragListener = require( 'SCENERY/listeners/DragListener' ); + const Emitter = require( 'AXON/Emitter' ); + const inherit = require( 'PHET_CORE/inherit' ); + const Node = require( 'SCENERY/nodes/Node' ); + const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const PaperNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/PaperNumber' ); + const Rectangle = require( 'SCENERY/nodes/Rectangle' ); + + /** + * @constructor + * + * @param {PaperNumber} paperNumber + * @param {Property.} 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 ) { + const 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( { parameters: [ { valueType: PaperNumberNode } ] } ); + + // @public {Emitter} - Triggered with self when this paper number node is split + this.splitEmitter = new Emitter( { parameters: [ { valueType: PaperNumberNode } ] } ); + + // @public {Emitter} - Triggered when user interaction with this paper number begins. + this.interactionStartedEmitter = new Emitter( { parameters: [ { 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, 0, 0, { + 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; } + + const viewPosition = self.globalToParentPoint( event.pointer.point ); + + // Determine how much (if any) gets moved off + const pulledPlace = paperNumber.getBaseNumberAt( self.parentToLocalPoint( viewPosition ) ).place; + const amountToRemove = ArithmeticRules.pullApartNumbers( paperNumber.numberValueProperty.value, pulledPlace ); + const 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 ); + + const 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(); + } + }; + } + + numberPlay.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() { + const self = this; + + const 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 ), reversedBaseNumbers.length > 1 ); + } ); + + // Grab the bounds of the biggest base number for the full bounds + const 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; + self.splitTarget.mouseArea = self.moveTarget.touchArea = self.moveTarget.rectBounds = new Bounds2( 0, 0, 0, 0 ); + } + else { + self.splitTarget.visible = true; + + // Locate the boundary between the "move" input area and "split" input area. + const 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 { + const 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.} allPaperNumberNodes + * @returns {Array} + */ + findAttachableNodes: function( allPaperNumberNodes ) { + const self = this; + const 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 ); + } + } ); +} );