Skip to content

Commit

Permalink
Add the number pieces carousel, see #2
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisklus committed Nov 20, 2019
1 parent 8d80cc0 commit fd0289a
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 7 deletions.
89 changes: 88 additions & 1 deletion js/lab/model/LabModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,24 @@ define( require => {
// modules
const BooleanProperty = require( 'AXON/BooleanProperty' );
const EnumerationProperty = require( 'AXON/EnumerationProperty' );
const FractionsCommonConstants = require( 'FRACTIONS_COMMON/common/FractionsCommonConstants' );
const numberPlay = require( 'NUMBER_PLAY/numberPlay' );
const NumberPiece = require( 'FRACTIONS_COMMON/building/model/NumberPiece' );
const NumberProperty = require( 'AXON/NumberProperty' );
const NumberStack = require( 'FRACTIONS_COMMON/building/model/NumberStack' );
const ObjectsPlayArea = require( 'NUMBER_PLAY/common/model/ObjectsPlayArea' );
const ObservableArray = require( 'AXON/ObservableArray' );
const OnesPlayArea = require( 'NUMBER_PLAY/common/model/OnesPlayArea' );
const PlayObjectType = require( 'NUMBER_PLAY/common/model/PlayObjectType' );
const Range = require( 'DOT/Range' );
const Vector2 = require( 'DOT/Vector2' );

const NUMBER_PIECE_RETURN_THRESHOLD = 92;

/**
* @constructor
*/
class LabModel {
class LabModel {

/**
* @param {number} highestCount - the highest integer number that can be counted to
Expand All @@ -32,6 +38,20 @@ define( require => {
*/
constructor( highestCount, paperNumberOrigin, objectMaxScale, tandem ) {

// @public {Array.<NumberStack>}
this.numberStacks = [];

// @public {ObservableArray.<NumberPiece>} - Number pieces in the play area (controlled or animating)
this.activeNumberPieces = new ObservableArray();

// Number stacks
_.range( 1, 21 ).map( number => {
const stack = new NumberStack( number, 2, false );
stack.numberPieces.push( new NumberPiece( number ) );
stack.numberPieces.push( new NumberPiece( number ) );
this.numberStacks.push( stack );
} );

const bucketOffsetX = 140;

// @public (read-only) - the model for managing paper ones in the playArea
Expand Down Expand Up @@ -61,6 +81,71 @@ define( require => {
);
}

/**
* Called when the user drags a number piece from a stack.
*
* @param {NumberPiece} numberPiece
* @public
*/
dragNumberPieceFromStack( numberPiece ) {
this.activeNumberPieces.push( numberPiece );
}

/**
* Returns a corresponding NumberStack that should be used as the "home" of a given NumberPiece (if it's returned from
* the play area with an animation, etc.)
* @public
*
* @param {NumberPiece} numberPiece
* @returns {NumberStack|null}
*/
findMatchingNumberStack( numberPiece ) {
return _.find( this.numberStacks, stack => stack.number === numberPiece.number ) || null;
}

/**
* Animates a piece back to its "home" stack.
* @public
*
* @param {NumberPiece} numberPiece
*/
returnActiveNumberPiece( numberPiece ) {
const numberStack = this.findMatchingNumberStack( numberPiece );
const offset = NumberStack.getOffset( numberStack.numberPieces.length );
numberPiece.animator.animateTo( {
position: numberStack.positionProperty.value.plus( offset.timesScalar( FractionsCommonConstants.NUMBER_BUILD_SCALE ) ),
scale: 1,
animationInvalidationProperty: numberStack.positionProperty,
endAnimationCallback: () => {
this.activeNumberPieces.remove( numberPiece );
if ( numberStack.isMutable ) {
numberStack.numberPieces.push( numberPiece );
}
}
} );
}

/**
* Called when a NumberPiece is dropped by the user.
* @public
*
* @param {NumberPiece} numberPiece
* @param {number} threshold - How much distance to allow between the piece and a container/group for it to be
* dropped inside.
* @param {boolean} animateReturn
*/
numberPieceDropped( numberPiece, threshold, animateReturn ) {
const numberPiecePosition = numberPiece.positionProperty.value;
const sortedNumberStacks = _.sortBy( this.numberStacks, numberStack => {
return numberStack.positionProperty.value.distance( numberPiecePosition );
} );
const closestNumberStack = sortedNumberStacks.shift();

if ( numberPiecePosition.distance( closestNumberStack.positionProperty.value ) < NUMBER_PIECE_RETURN_THRESHOLD ) {
animateReturn ? this.returnActiveNumberPiece( numberPiece ) : this.activeNumberPieces.remove( numberPiece );
}
}

/**
* Resets the model.
* @public
Expand All @@ -69,6 +154,7 @@ define( require => {
this.onesPlayArea.reset();
this.leftObjectsPlayArea.reset();
this.rightObjectsPlayArea.reset();
this.activeNumberPieces.reset();
}

/**
Expand All @@ -78,6 +164,7 @@ define( require => {
*/
step( dt ) {
this.onesPlayArea.step( dt );
this.activeNumberPieces.forEach( numberPiece => numberPiece.step( dt ) );
}
}

Expand Down
53 changes: 53 additions & 0 deletions js/lab/view/LabNumberCarousel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2019, University of Colorado Boulder

/**
* The top carousel with number pieces for the Lab screen.
*
* @author Chris Klusendorf (PhET Interactive Simulations)
*/
define( require => {
'use strict';

// modules
const numberPlay = require( 'NUMBER_PLAY/numberPlay' );
const Carousel = require( 'SUN/Carousel' );
const Node = require( 'SCENERY/nodes/Node' );
const StackNodesBox = require( 'FRACTIONS_COMMON/building/view/StackNodesBox' );

class LabNumberCarousel extends Carousel {
/**
* @param {Array.<NumberStack>} numberStacks
* @param {number} animationDuration
* @param {function} pressCallback - function( {Event}, {Stack} ) - Called when a press is started.
*/
constructor( numberStacks, animationDuration, pressCallback ) {
const box = new StackNodesBox( [
...numberStacks
], pressCallback );

super( box.children.map( stack => {
return new Node().addChild( stack );
} ), {
itemsPerPage: 10,
margin: 14,
spacing: 8,
animationDuration: animationDuration
} );

// @private {StackNodesBox}
this.box = box;
}

/**
* Sets the model positions of our model objects corresponding to their displayed (view) positions.
* @public
*
* @param {ModelViewTransform2} modelViewTransform
*/
updateModelLocations( modelViewTransform ) {
this.box.updateModelLocations( modelViewTransform, this );
}
}

return numberPlay.register( 'LabNumberCarousel', LabNumberCarousel );
} );
109 changes: 103 additions & 6 deletions js/lab/view/LabScreenView.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ define( require => {

// modules
const Bounds2 = require( 'DOT/Bounds2' );
const DragListener = require( 'SCENERY/listeners/DragListener' );
const LabNumberCarousel = require( 'NUMBER_PLAY/lab/view/LabNumberCarousel' );
const ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' );
const Node = require( 'SCENERY/nodes/Node' );
const numberPlay = require( 'NUMBER_PLAY/numberPlay' );
const NumberPlayConstants = require( 'NUMBER_PLAY/common/NumberPlayConstants' );
const NumberPiece = require( 'FRACTIONS_COMMON/building/model/NumberPiece' );
const NumberPieceNode = require( 'FRACTIONS_COMMON/building/view/NumberPieceNode' );
const ObjectsPlayAreaNode = require( 'NUMBER_PLAY/common/view/ObjectsPlayAreaNode' );
const OnesPlayAreaNode = require( 'NUMBER_PLAY/common/view/OnesPlayAreaNode' );
const ResetAllButton = require( 'SCENERY_PHET/buttons/ResetAllButton' );
Expand All @@ -31,52 +36,144 @@ define( require => {
tandem: tandem
} );

// @private
this.model = model;

const playAreaViewBounds = new Bounds2(
this.layoutBounds.left,
this.layoutBounds.top + NumberPlayConstants.SCREEN_VIEW_Y_PADDING,
this.layoutBounds.right,
this.layoutBounds.bottom - NumberPlayConstants.SCREEN_VIEW_Y_PADDING
);
const translateMVT = ModelViewTransform2.createSinglePointScaleInvertedYMapping(
this.modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping(
Vector2.ZERO,
playAreaViewBounds.centerBottom,
1
);

// @private {Array.<NumberPieceNode>}
this.numberPieceNodes = [];

const animationDuration = 0.4; // in seconds

// @private {Node}
this.numberPanel = new LabNumberCarousel( model.numberStacks, animationDuration, ( event, stack ) => {
const modelPoint = this.modelViewTransform.viewToModelPosition( this.globalToLocalPoint( event.pointer.point ) );
const numberPiece = new NumberPiece( stack.number );
numberPiece.positionProperty.value = modelPoint;
model.dragNumberPieceFromStack( numberPiece );
const numberPieceNode = this.getNumberPieceNode( numberPiece );
numberPieceNode.dragListener.press( event, numberPieceNode );
} );
this.numberPanel.centerX = this.layoutBounds.centerX;
this.numberPanel.top = playAreaViewBounds.top;

this.numberPanel.pageNumberProperty.link( pageNumber => {

// TODO: The following code needs to run when the Carousel is actually at the page that this Property says it's
// at, but this is not an acceptable solution.
setTimeout( () => {
this.numberPanel.updateModelLocations( this.modelViewTransform );
}, animationDuration * 1000 );
} );

model.activeNumberPieces.addItemAddedListener( this.addNumberPiece.bind( this ) );
model.activeNumberPieces.addItemRemovedListener( this.removeNumberPiece.bind( this ) );

// create and add the OnesPlayAreaNode
const onesPlayAreaNode = new OnesPlayAreaNode( model.onesPlayArea, playAreaViewBounds, translateMVT );
const onesPlayAreaNode = new OnesPlayAreaNode( model.onesPlayArea, playAreaViewBounds, this.modelViewTransform );
this.addChild( onesPlayAreaNode );

// the one's play area covers other nodes, so add the numberPanel after
this.addChild( this.numberPanel );

// create and add the left ObjectsPlayAreaNode
const playAreaModelBounds = translateMVT.viewToModelBounds( playAreaViewBounds ).dilatedX( -20 ).dilatedY( -19 );
const playAreaModelBounds = this.modelViewTransform.viewToModelBounds( playAreaViewBounds ).dilatedX( -20 ).dilatedY( -19 );
const leftObjectsPlayAreaNode = new ObjectsPlayAreaNode(
model.leftObjectsPlayArea,
playAreaModelBounds,
translateMVT
this.modelViewTransform
);
this.addChild( leftObjectsPlayAreaNode );

// create and add the right ObjectsPlayAreaNode
const rightObjectsPlayAreaNode = new ObjectsPlayAreaNode(
model.rightObjectsPlayArea,
playAreaModelBounds,
translateMVT
this.modelViewTransform
);
this.addChild( rightObjectsPlayAreaNode );

// TODO: all pieces in this ScreenView need to use this layer, not just the number pieces
this.pieceLayer = new Node();
this.addChild( this.pieceLayer );

// 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' )
} );
this.addChild( resetAllButton );
}

/**
* Returns the corresponding NumberPieceNode for a given NumberPiece.
* @public
*
* @param {NumberPiece} numberPiece
* @returns {NumberPieceNode}
*/
getNumberPieceNode( numberPiece ) {
return _.find( this.numberPieceNodes, numberPieceNode => numberPieceNode.numberPiece === numberPiece );
}

/**
* Called when a new NumberPiece is added to the model (we'll create the view).
* @private
*
* @param {NumberPiece} numberPiece
*/
addNumberPiece( numberPiece ) {
const numberPieceNode = new NumberPieceNode( numberPiece, {
positioned: true,
modelViewTransform: this.modelViewTransform,
dropListener: wasTouch => {
this.model.numberPieceDropped(
numberPiece,
wasTouch ? 50 : 20,
( numberPiece.number < 11 && this.numberPanel.pageNumberProperty.value === 0 ) ||
( numberPiece.number > 10 && this.numberPanel.pageNumberProperty.value === 1 )
);
}
} );

numberPieceNode.cursor = 'pointer';
numberPieceNode.inputListeners = [ DragListener.createForwardingListener( event => {
numberPieceNode.dragListener.press( event, numberPieceNode );
} ) ];

this.numberPieceNodes.push( numberPieceNode );
this.pieceLayer.addChild( numberPieceNode );
}

/**
* Called when a NumberPiece is removed from the model (we'll remove the view).
* @private
*
* @param {NumberPiece} numberPiece
*/
removeNumberPiece( numberPiece ) {
const numberPieceNode = _.find( this.numberPieceNodes, numberPieceNode => numberPieceNode.numberPiece === numberPiece );

_.pull( this.numberPieceNodes, numberPieceNode );
this.pieceLayer.removeChild( numberPieceNode );
numberPieceNode.dispose();
}
}

return numberPlay.register( 'LabScreenView', LabScreenView );
Expand Down
1 change: 1 addition & 0 deletions js/number-play-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require.config( {
AXON: '../../axon/js',
BRAND: '../../brand/' + phet.chipper.brand + '/js',
DOT: '../../dot/js',
FRACTIONS_COMMON: '../../fractions-common/js',
JOIST: '../../joist/js',
KITE: '../../kite/js',
MAKE_A_TEN: '../../make-a-ten/js',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"phet": {
"requirejsNamespace": "NUMBER_PLAY",
"phetLibs": [
"fractions-common",
"make-a-ten",
"twixt"
],
Expand Down

0 comments on commit fd0289a

Please sign in to comment.