Skip to content

Commit

Permalink
Add preferences model and structure for portrayals. Remove unused cod…
Browse files Browse the repository at this point in the history
…e in re-centering trash can image that was previously offset. See #304.
  • Loading branch information
Luisav1 committed Jan 24, 2024
1 parent c954cb4 commit 74cf55d
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 45 deletions.
5 changes: 5 additions & 0 deletions forces-and-motion-basics_en.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
"phet-io",
"adapted-from-phet"
],
"simFeatures": {
"supportedRegionsAndCultures": [
"usa"
]
},
"simulation": true,
"phet-io": {
"validation": false,
Expand Down
9 changes: 5 additions & 4 deletions js/forces-and-motion-basics-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import { Image } from '../../scenery/js/imports.js';
import Tandem from '../../tandem/js/Tandem.js';
import accelerationIcon_png from '../images/accelerationIcon_png.js';
import frictionIcon_png from '../images/frictionIcon_png.js';
import motionIcon_png from '../images/motionIcon_png.js';
import tugIcon_png from '../images/tugIcon_png.js';
import ForcesAndMotionBasicsStrings from './ForcesAndMotionBasicsStrings.js';
import MotionScreen from './motion/MotionScreen.js';
import PreferencesModelSingleton from './motion/PreferencesModelSingleton.js';
import MassPlayerImages from './motion/view/MassPlayerImages.js';
import MotionScreenIcon from './motion/view/MotionScreenIcon.js';
import NetForceModel from './netforce/model/NetForceModel.js';
import NetForceScreenView from './netforce/view/NetForceScreenView.js';

Expand All @@ -27,6 +29,7 @@ const forcesAndMotionBasicsTitleStringProperty = ForcesAndMotionBasicsStrings[ '
const tandem = Tandem.ROOT;

const simOptions = {
preferencesModel: PreferencesModelSingleton,
credits: {
leadDesign: 'Ariel Paul, Noah Podolefsky',
graphicArts: 'Mariah Hermsmeyer, Sharon Siman-Tov',
Expand Down Expand Up @@ -63,9 +66,7 @@ simLauncher.launch( () => {

const motionScreen = new MotionScreen( 'motion', motionScreenTandem, {
name: ForcesAndMotionBasicsStrings.motionStringProperty,
homeScreenIcon: new ScreenIcon( new Image( motionIcon_png, {
tandem: motionScreenTandem.createTandem( 'icon' )
} ), screenIconOptions )
homeScreenIcon: new MotionScreenIcon( MassPlayerImages.MASS_PLAYER_PORTRAYALS, screenIconOptions, motionScreenTandem )
} );

const frictionScreen = new MotionScreen( 'friction', frictionScreenTandem, {
Expand Down
20 changes: 20 additions & 0 deletions js/motion/PreferencesModelSingleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2024, University of Colorado Boulder

/**
* Preferences model as a singleton, so it can be accessed by the HumanTypeEnum.
*
* @author Luisa Vargas
*/

import PreferencesModel from '../../../joist/js/preferences/PreferencesModel.js';
import forcesAndMotionBasics from '../forcesAndMotionBasics.js';
import MassPlayerImages from './view/MassPlayerImages.js';

const PreferencesModelSingleton = new PreferencesModel( {
localizationOptions: {
portrayals: MassPlayerImages.MASS_PLAYER_PORTRAYALS
}
} );

forcesAndMotionBasics.register( 'PreferencesModelSingleton', PreferencesModelSingleton );
export default PreferencesModelSingleton;
67 changes: 67 additions & 0 deletions js/motion/model/HumanTypeEnum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2024, University of Colorado Boulder

/**
* HumanTypeEnum identifies the human object type and sets the holding, standing, or sitting image for the human.
*
* @author Luisa Vargas
*/

import MappedProperty from '../../../../axon/js/MappedProperty.js';
import EnumerationValue from '../../../../phet-core/js/EnumerationValue.js';
import Enumeration from '../../../../phet-core/js/Enumeration.js';
import forcesAndMotionBasics from '../../forcesAndMotionBasics.js';
import PreferencesModelSingleton from '../PreferencesModelSingleton.js';

class HumanTypeEnum extends EnumerationValue {

static GIRL = new HumanTypeEnum( 'girl' );
static MAN = new HumanTypeEnum( 'man' );

static enumeration = new Enumeration( HumanTypeEnum );

/**
* @param {string} humanType
*/
constructor( humanType ) {
super();

this.holdingImageProperty = new MappedProperty( PreferencesModelSingleton.localizationModel.regionAndCulturePortrayalProperty, {
map: portrayal => {
if ( humanType === 'girl' ) {
return portrayal.girlHolding;
}
else {
assert && assert( humanType === 'man', 'Human type must be girl, or man, but it is ', humanType );
return portrayal.manHolding;
}
}
} );

this.standingImageProperty = new MappedProperty( PreferencesModelSingleton.localizationModel.regionAndCulturePortrayalProperty, {
map: portrayal => {
if ( humanType === 'girl' ) {
return portrayal.girlStanding;
}
else {
assert && assert( humanType === 'man', 'Human type must be girl, or man, but it is ', humanType );
return portrayal.manStanding;
}
}
} );

this.sittingImageProperty = new MappedProperty( PreferencesModelSingleton.localizationModel.regionAndCulturePortrayalProperty, {
map: portrayal => {
if ( humanType === 'girl' ) {
return portrayal.girlSitting;
}
else {
assert && assert( humanType === 'man', 'Human type must be girl, or man, but it is ', humanType );
return portrayal.manSitting;
}
}
} );
}
}

forcesAndMotionBasics.register( 'HumanTypeEnum', HumanTypeEnum );
export default HumanTypeEnum;
34 changes: 23 additions & 11 deletions js/motion/model/Item.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import IOType from '../../../../tandem/js/types/IOType.js';
import ObjectLiteralIO from '../../../../tandem/js/types/ObjectLiteralIO.js';
import ReferenceIO from '../../../../tandem/js/types/ReferenceIO.js';
import forcesAndMotionBasics from '../../forcesAndMotionBasics.js';
import HumanTypeEnum from './HumanTypeEnum.js';

class Item extends PhetioObject {

/**
* Constructor for Item
*
* @param {MotionModel || NetForceModel} context - model context in which this item exists
* @param {string} name - string describing this type of item
* @param {string | HumanTypeEnum } name - string describing this type of item, or HumanTypeEnum of this human item
* @param {Tandem} tandem
* @param {image} image - image from the 'image!' plugin, representing the item
* @param {number} mass - model mass of the item
Expand All @@ -47,20 +48,36 @@ class Item extends PhetioObject {
phetioState: false
} );

this.name = name;
this.name = typeof name === 'string' ? name : name.name.toLowerCase();

// Set the standing, sitting, and holding image properties if item is human
let standingImageProperty;
let sittingImageProperty;
let holdingImageProperty;
if ( name === HumanTypeEnum.GIRL ) {
standingImageProperty = HumanTypeEnum.GIRL.standingImageProperty;
sittingImageProperty = HumanTypeEnum.GIRL.sittingImageProperty;
holdingImageProperty = HumanTypeEnum.GIRL.holdingImageProperty;
}
else if ( name === HumanTypeEnum.MAN ) {
standingImageProperty = HumanTypeEnum.MAN.standingImageProperty;
sittingImageProperty = HumanTypeEnum.MAN.sittingImageProperty;
holdingImageProperty = HumanTypeEnum.MAN.holdingImageProperty;
}

//Non-observable properties
this.initialX = x;
this.initialY = y;
this.image = image;
this.mass = mass;
this.pusherInset = pusherInset;
this.sittingImageNode = sittingImage;
this.holdingImage = holdingImage;
this.context = context;
this.mystery = mystery;
this.homeScale = homeScale || 1.0;

this.imageProperty = typeof name === 'string' ? new Property( image ) : standingImageProperty;
this.sittingImageProperty = typeof name === 'string' ? new Property( sittingImage ) : sittingImageProperty;
this.holdingImageProperty = typeof name === 'string' ? new Property( holdingImage ) : holdingImageProperty;

// @public - the position of the item
this.positionProperty = new Vector2Property( new Vector2( x, y ), {
tandem: tandem.createTandem( 'positionProperty' )
Expand Down Expand Up @@ -112,17 +129,12 @@ class Item extends PhetioObject {
this.context.directionProperty.link( direction => {

//only change directions if on the board, and always choose one of left/right, and only for people
if ( this.onBoardProperty.get() && direction !== 'none' && sittingImage ) {
if ( this.onBoardProperty.get() && direction !== 'none' && ( name === HumanTypeEnum.GIRL || name === HumanTypeEnum.MAN ) ) {
this.directionProperty.set( direction );
}
} );
}

//For unknown reasons, the trash can is not centered when drawn, so we make up for it with a workaround here
get centeringOffset() {
return this.image === 'trashCan.png' ? 5 : 0;
}

// @public - Return true if the arms should be up (for a human)
armsUp() {
return this.context.draggingItems().length > 0 || this.context.isItemStackedAbove( this );
Expand Down
13 changes: 4 additions & 9 deletions js/motion/model/MotionModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,10 @@ import crate_png from '../../../images/crate_png.js';
import fridge_png from '../../../images/fridge_png.js';
import mysteryObject01_png from '../../../images/mysteryObject01_png.js';
import waterBucket_png from '../../../images/waterBucket_png.js';
import girlHolding_png from '../../../mipmaps/girlHolding_png.js';
import girlSitting_png from '../../../mipmaps/girlSitting_png.js';
import girlStanding_png from '../../../mipmaps/girlStanding_png.js';
import manHolding_png from '../../../mipmaps/manHolding_png.js';
import manSitting_png from '../../../mipmaps/manSitting_png.js';
import manStanding_png from '../../../mipmaps/manStanding_png.js';
import trashCan_png from '../../../mipmaps/trashCan_png.js';
import forcesAndMotionBasics from '../../forcesAndMotionBasics.js';
import MotionConstants from '../MotionConstants.js';
import HumanTypeEnum from './HumanTypeEnum.js';
import Item from './Item.js';

class MotionModel {
Expand Down Expand Up @@ -235,8 +230,8 @@ class MotionModel {
const fridge = new Item( this, 'fridge', tandem.createTandem( 'fridge' ), fridge_png, 200, leftmostItemXLeft, 437, 0.8, 1.1, 4 );
const crate1 = new Item( this, 'crate1', tandem.createTandem( 'crate1' ), crate_png, 50, leftmostItemXLeft + crate1Spacing, 507, 0.5 );
const crate2 = new Item( this, 'crate2', tandem.createTandem( 'crate2' ), crate_png, 50, leftmostItemXLeft + crate1Spacing + crate2Spacing, 507, 0.5 );
const girl = new Item( this, 'girl', tandem.createTandem( 'girl' ), girlStanding_png, 40, leftmostItemXRight, 465, 0.6, 1.0, 4.2, girlSitting_png, girlHolding_png[ 1 ].img );
const man = new Item( this, 'man', tandem.createTandem( 'man' ), manStanding_png, 80, leftmostItemXRight + manSpacing, 428, 0.6, 0.92, 5, manSitting_png, manHolding_png );
const girl = new Item( this, HumanTypeEnum.GIRL, tandem.createTandem( 'girl' ), undefined, 40, leftmostItemXRight, 465, 0.6, 1.0, 4.2 );
const man = new Item( this, HumanTypeEnum.MAN, tandem.createTandem( 'man' ), undefined, 80, leftmostItemXRight + manSpacing, 428, 0.6, 0.92, 5 );
const trashCan = new Item( this, 'trash', tandem.createTandem( 'trash' ), trashCan_png, 100, leftmostItemXRight + manSpacing + trashSpacing, 496, 0.7, 1.0, 5 );
const mysteryBox = new Item( this, 'mystery', tandem.createTandem( 'mystery' ), mysteryObject01_png, 50, leftmostItemXRight + manSpacing + trashSpacing + mysterySpacing, 513, 0.3, 1.0, undefined, undefined, undefined, true );
const bucket = new Item( this, 'bucket', tandem.createTandem( 'bucket' ), waterBucket_png, 100, leftmostItemXRight + manSpacing + trashSpacing + mysterySpacing + bucketSpacing, 547 + -35, 0.68, 1.0, 8 );
Expand Down Expand Up @@ -312,7 +307,7 @@ class MotionModel {
for ( let i = 0; i < this.stackObservableArray.length; i++ ) {
const size = this.view.getSize( this.stackObservableArray.get( i ) );
sumHeight += size.height;
this.stackObservableArray.get( i ).animateTo( this.view.layoutBounds.width / 2 - size.width / 2 + this.stackObservableArray.get( i ).centeringOffset, ( this.skateboard ? 334 : 360 ) - sumHeight, 'stack' );//TODO: factor out this code for layout, which is duplicated in MotionTab.topOfStack https://github.com/phetsims/tasks/issues/1129
this.stackObservableArray.get( i ).animateTo( this.view.layoutBounds.width / 2 - size.width / 2, ( this.skateboard ? 334 : 360 ) - sumHeight, 'stack' );//TODO: factor out this code for layout, which is duplicated in MotionTab.topOfStack https://github.com/phetsims/tasks/issues/1129
}
}

Expand Down
28 changes: 16 additions & 12 deletions js/motion/view/ItemNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Image, Node, Rectangle, SimpleDragHandler, Text } from '../../../../sce
import Tandem from '../../../../tandem/js/Tandem.js';
import forcesAndMotionBasics from '../../forcesAndMotionBasics.js';
import ForcesAndMotionBasicsStrings from '../../ForcesAndMotionBasicsStrings.js';
import PreferencesModelSingleton from '../PreferencesModelSingleton.js';

const pattern0MassUnitsKilogramsString = ForcesAndMotionBasicsStrings.pattern[ '0massUnitsKilograms' ];

Expand All @@ -27,13 +28,14 @@ class ItemNode extends Node {
* @param {MotionModel} model the entire model for the containing screen
* @param {MotionScreenView} motionView the entire view for the containing screen
* @param {Item} item the corresponding to this ItemNode
* @param {Image} normalImage the phet.scenery.Image to show for this node
* @param {Image} sittingImage optional image for when the person is sitting down
* @param {Image} holdingImage optional image for when the person is holding an object
* @param {Property<Image>} normalImageProperty property for the phet.scenery.Image to show for this node
* @param {Property<Image>} sittingImageProperty property fot optional sitting image for when the person is sitting down
* @param {Property<Image>} holdingImageProperty property for optional holding image for when the person is holding an object
* @param {Property} showMassesProperty property for whether the mass value should be shown
* @param {Rectangle} itemToolbox - The toolbox that contains this item
* @param {Tandem} tandem
*/
constructor( model, motionView, item, normalImage, sittingImage, holdingImage, showMassesProperty, itemToolbox, tandem ) {
constructor( model, motionView, item, normalImageProperty, sittingImageProperty, holdingImageProperty, showMassesProperty, itemToolbox, tandem ) {

super( {
cursor: 'pointer',
Expand All @@ -50,24 +52,24 @@ class ItemNode extends Node {
this.translate( item.positionProperty.get() );

//Create the node for the main graphic
const normalImageNode = new Image( normalImage, { tandem: tandem.createTandem( 'normalImageNode' ) } );
const normalImageNode = new Image( normalImageProperty.value, { tandem: tandem.createTandem( 'normalImageNode' ) } );
this.normalImageNode = normalImageNode;

// keep track of the sitting image to track its width for the pusher
// @public (read-only)
this.sittingImageNode = new Image( sittingImage, { tandem: tandem.createTandem( 'sittingImageNode' ) } );
this.sittingImageNode = new Image( sittingImageProperty.value, { tandem: tandem.createTandem( 'sittingImageNode' ) } );

//When the model changes, update the image position as well as which image is shown
const updateImage = () => {
// var centerX = normalImageNode.centerX;
if ( ( typeof holdingImage !== 'undefined' ) && ( item.armsUp() && item.onBoardProperty.get() ) ) {
normalImageNode.image = holdingImage;
if ( ( typeof holdingImageProperty.value !== 'undefined' ) && ( item.armsUp() && item.onBoardProperty.get() ) ) {
normalImageNode.image = holdingImageProperty.value;
}
else if ( item.onBoardProperty.get() && typeof sittingImage !== 'undefined' ) {
normalImageNode.image = sittingImage;
else if ( item.onBoardProperty.get() && typeof sittingImageProperty.value !== 'undefined' ) {
normalImageNode.image = sittingImageProperty.value;
}
else {
normalImageNode.image = normalImage;
normalImageNode.image = normalImageProperty.value;
}
if ( this.labelNode ) {
this.updateLabelPosition();
Expand All @@ -90,7 +92,7 @@ class ItemNode extends Node {
const moveToStack = () => {
item.onBoardProperty.set( true );
const imageWidth = item.getCurrentScale() * normalImageNode.width;
item.animateTo( motionView.layoutBounds.width / 2 - imageWidth / 2 + item.centeringOffset, motionView.topOfStack - this.height, 'stack' );
item.animateTo( motionView.layoutBounds.width / 2 - imageWidth / 2, motionView.topOfStack - this.height, 'stack' );
model.stackObservableArray.add( item );
if ( model.stackObservableArray.length > 3 ) {
model.spliceStackBottom();
Expand Down Expand Up @@ -250,6 +252,8 @@ class ItemNode extends Node {
this.addChild( labelText );

showMassesProperty.link( showMasses => { labelText.visible = showMasses; } );

PreferencesModelSingleton.localizationModel.regionAndCulturePortrayalProperty.link( updateImage );
}


Expand Down
19 changes: 19 additions & 0 deletions js/motion/view/MassPlayerImages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2024, University of Colorado Boulder

/**
* MassPlayerImages contains an array of character sets, each representing a different region/culture.
*
* @author Luisa Vargas
*/

import forcesAndMotionBasics from '../../forcesAndMotionBasics.js';
import MassPlayerPortrayalUSA from './MassPlayerPortrayalUSA.js';

const MassPlayerImages = {
MASS_PLAYER_PORTRAYALS: [
MassPlayerPortrayalUSA
]
};

forcesAndMotionBasics.register( 'MassPlayerImages', MassPlayerImages );
export default MassPlayerImages;
43 changes: 43 additions & 0 deletions js/motion/view/MassPlayerPortrayal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2024, University of Colorado Boulder

/**
* The MassPlayerPortrayal defines what is needed for each portrayal in Forces and Motion: Basics.
*
* @author Luisa Vargas
*
*/

import RegionAndCulturePortrayal from '../../../../joist/js/preferences/RegionAndCulturePortrayal.js';
import forcesAndMotionBasics from '../../forcesAndMotionBasics.js';

export default class MassPlayerPortrayal extends RegionAndCulturePortrayal {

/**
* @param label { LocalizedStringProperty }
* @param girlHolding { HTMLImageElement }
* @param girlSitting { HTMLImageElement }
* @param girlStanding { HTMLImageElement }
* @param manHolding { HTMLImageElement }
* @param manSitting { HTMLImageElement }
* @param manStanding { HTMLImageElement }
* @param screenIcon { HTMLImageElement }
* @param queryParameterValue { string }
*/
constructor( label,
girlHolding, girlSitting, girlStanding,
manHolding, manSitting, manStanding,
screenIcon, queryParameterValue ) {

super( label, queryParameterValue, {} );

this.girlHolding = girlHolding;
this.girlSitting = girlSitting;
this.girlStanding = girlStanding;
this.manHolding = manHolding;
this.manStanding = manStanding;
this.manSitting = manSitting;
this.screenIcon = screenIcon;
}
}

forcesAndMotionBasics.register( 'MassPlayerPortrayal', MassPlayerPortrayal );
Loading

0 comments on commit 74cf55d

Please sign in to comment.