phetsims / normal-modes

"Normal Modes" is an educational simulation in HTML5, by PhET Interactive Simulations.
GNU General Public License v3.0
0 stars 2 forks source link

Replace "Speed" control with TimeControlNode #58

Closed pixelzoom closed 3 years ago

pixelzoom commented 3 years ago

Related to #57...

Normal Modes currently has a "Speed" control that looks like the one in the legacy (Flash) version:

screenshot_751

PhET's HTML5 sims have standardized on a general time control. It's TimeControlNode, and it lives in the scenery-phet repository. Run the scenery-phet demo (via phetmarks), go to the Components screen, choose TimeControlNode from the combo box, and you'll see some examples:

screenshot_752

normal-modes needs to be converted to TimeControlNode. That involves:

This would be a good first task for @issamali67. Consult with @arouinfar and @ariel-phet on design questions. Feel free to consult with me on TimeControlNode options and implementation issues.

pixelzoom commented 3 years ago

Discussed at 2/4/21 design meeting. @DianaTavares is going to provide mockups showing where the time control should appear, and how it should look.

DianaTavares commented 3 years ago

This is the mockup:

image

and this how it looks in the full simulation, screen 1 and 2: image

image

pixelzoom commented 3 years ago

Something that @issamali67 and I discussed on Slack that should be captured here.

OneDimensionModel and TwoDimensionsModel both define this "speed" like this:

    // @public {Property.<number>} determines the speed at which the sim plays
    this.simSpeedProperty = new NumberProperty( OneDimensionConstants.INIT_SPEED, {
      tandem: tandem.createTandem( 'simSpeedProperty' ),
      range: new Range( OneDimensionConstants.MIN_SPEED, OneDimensionConstants.MAX_SPEED )
    } );

TimeControlNode specifies it's speed option like this:

      // {EnumerationProperty.<TimeSpeed>|null} - Play speed Property for the radio button group. If null,
      // no radio buttons included in this control.
      timeSpeedProperty: null,

Because they are different types, this will not work:

new TimeControlNode( ..., {
  ...
  timeSpeedProperty: model.simSpeedProperty
} );

So in addition to using TimeControlNode in the view, you'll need to adjust the model. Here's one way to do that (untested, but the right direction).

As shown in the mockups, we need to support two speeds, "Normal" and "Slow". In NormalModesConstants.js, add these constants. They are the multipliers that will be applied to dt in the model's step method.

    NORMAL_SPEED: 1,
    SLOW_SPEED: 0.2,

Then in OneDimensionalModel.js, define a new timeSpeedProperty that you'll pass to TimeControlNode:

    // @public used by the time control to select a speed
    this.timeSpeedProperty = new EnumerationProperty( TimeSpeed, TimeSpeed.NORMAL, {
      validValues: [ TimeSpeed.NORMAL, TimeSpeed.SLOW ]
    } );

Also in OneDimensionalModel.js, change simSpeedProperty to be derived from timeSpeedProperty:

// @private {DerivedProperty.<number>} multiplier for dt used in step
this.simSpeedProperty = new DerivedProperty(
  [ this.timeSpeedProperty ],
    timeSpeed => ( timeSpeed === TimeSpeed.NORMAL ) ? NormalModesConstants.NORMAL_SPEED : NormalModesConstants.SLOW_SPEED
);

Unfortunately you'll need to duplicate whatever you do in both OneDimensionalModel.js and TwoDimensionalsModel.js. There's a lot of duplication there that needs to be factored out, see https://github.com/phetsims/normal-modes/issues/63.

And these constants, duplicated in OneDimensionalConstants.js and TwoDimensionalConstants.js, should be reviewed. Any of them that are no longer used should be deleted.

  MIN_SPEED: 0.02,
  INIT_SPEED: 1,
  MAX_SPEED: 3,
  DELTA_SPEED: 0.1,

EDIT: You could also do away with simSpeedProperty, and make this change in singleStep:

-    dt *= this.simSpeedProperty.get();
+    dt *= ( ( timeSpeed === TimeSpeed.NORMAL ) ? NormalModesConstants.NORMAL_SPEED : NormalModesConstants.SLOW_SPEED );
pixelzoom commented 3 years ago

@issamali67 contacted me on Slack and said:

Including this.simSpeedProperty = new DerivedProperty(…) in both 1d and 2d models generates a “assert.js:21 Assertion failed: linear requires a number to evaluate” error.

I assume I need to comment out the original this.simSpeedProperty = new Enumeration(…), which I did.

I think the problems is in singleStep. The this.simSpeedProperty.get() is unable to get the ‘timeSpeed’ and dt is not getting a number.

I also tried the one liner step. Somehow I need to grab timeSpeed from the ‘new’ construct.

I replied:

I'm afraid I can't debug that without a stack trace. I spent a couple of minutes and got the approach that I described to work. I'll put a patch in the GitHub issue. Perhaps you can inspect that patch and figure out what you're doing wrong. If not, let's set up a time to pair program.

Here's that patch:

Patch ```diff Index: js/two-dimensions/model/TwoDimensionsModel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/two-dimensions/model/TwoDimensionsModel.js b/js/two-dimensions/model/TwoDimensionsModel.js --- a/js/two-dimensions/model/TwoDimensionsModel.js (revision f77813581e4211dcc43b6079c218a47cf1d5dfb5) +++ b/js/two-dimensions/model/TwoDimensionsModel.js (date 1613088270220) @@ -13,6 +13,7 @@ import Property from '../../../../axon/js/Property.js'; import Range from '../../../../dot/js/Range.js'; import Vector2 from '../../../../dot/js/Vector2.js'; +import TimeSpeed from '../../../../scenery-phet/js/TimeSpeed.js'; import NumberIO from '../../../../tandem/js/types/NumberIO.js'; import AmplitudeDirection from '../../common/model/AmplitudeDirection.js'; import Mass from '../../common/model/Mass.js'; @@ -36,11 +37,16 @@ tandem: tandem.createTandem( 'playingProperty' ) } ); - // @public {Property.} determines the speed at which the sim plays - this.simSpeedProperty = new NumberProperty( TwoDimensionsConstants.INIT_SPEED, { - tandem: tandem.createTandem( 'simSpeedProperty' ), - range: new Range( TwoDimensionsConstants.MIN_SPEED, TwoDimensionsConstants.MAX_SPEED ) + // @public used by the time control to select a speed + this.timeSpeedProperty = new EnumerationProperty( TimeSpeed, TimeSpeed.NORMAL, { + validValues: [ TimeSpeed.NORMAL, TimeSpeed.SLOW ] } ); + + // @private {DerivedProperty.} multiplier applied to dt + this.timeScaleProperty = new DerivedProperty( + [ this.timeSpeedProperty ], + timeSpeed => ( timeSpeed === TimeSpeed.NORMAL ) ? NormalModesConstants.NORMAL_TIME_SCALE : NormalModesConstants.SLOW_TIME_SCALE + ); // @public {Property.} determines visibility of the springs this.springsVisibleProperty = new BooleanProperty( true, { @@ -309,7 +315,7 @@ reset() { this.playingProperty.reset(); this.timeProperty.reset(); - this.simSpeedProperty.reset(); + this.timeSpeedProperty.reset(); this.springsVisibleProperty.reset(); this.numberVisibleMassesProperty.reset(); this.draggingMassIndexesProperty.reset(); @@ -375,7 +381,7 @@ * @public */ singleStep( dt ) { - dt *= this.simSpeedProperty.get(); + dt *= this.timeScaleProperty.get(); this.timeProperty.set( this.timeProperty.get() + dt ); if ( this.draggingMassIndexesProperty.get() !== null ) { this.setVerletPositions( dt ); Index: js/common/NormalModesConstants.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/NormalModesConstants.js b/js/common/NormalModesConstants.js --- a/js/common/NormalModesConstants.js (revision f77813581e4211dcc43b6079c218a47cf1d5dfb5) +++ b/js/common/NormalModesConstants.js (date 1613088136803) @@ -33,8 +33,11 @@ // number of masses in a row for both 1d and 2d MIN_MASSES_ROW_LEN: 1, INIT_MASSES_ROW_LEN: 3, - MAX_MASSES_ROW_LEN: 10 + MAX_MASSES_ROW_LEN: 10, + // dt will be scaled by these amounts, depending on the speed selection for the sim. + NORMAL_TIME_SCALE: 1, + SLOW_TIME_SCALE: 0.2 }; normalModes.register( 'NormalModesConstants', NormalModesConstants ); Index: js/one-dimension/model/OneDimensionModel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/one-dimension/model/OneDimensionModel.js b/js/one-dimension/model/OneDimensionModel.js --- a/js/one-dimension/model/OneDimensionModel.js (revision f77813581e4211dcc43b6079c218a47cf1d5dfb5) +++ b/js/one-dimension/model/OneDimensionModel.js (date 1613088248813) @@ -12,6 +12,7 @@ import NumberProperty from '../../../../axon/js/NumberProperty.js'; import Range from '../../../../dot/js/Range.js'; import Vector2 from '../../../../dot/js/Vector2.js'; +import TimeSpeed from '../../../../scenery-phet/js/TimeSpeed.js'; import NumberIO from '../../../../tandem/js/types/NumberIO.js'; import AmplitudeDirection from '../../common/model/AmplitudeDirection.js'; import Mass from '../../common/model/Mass.js'; @@ -36,11 +37,16 @@ tandem: tandem.createTandem( 'playingProperty' ) } ); - // @public {Property.} determines the speed at which the sim plays - this.simSpeedProperty = new NumberProperty( OneDimensionConstants.INIT_SPEED, { - tandem: tandem.createTandem( 'simSpeedProperty' ), - range: new Range( OneDimensionConstants.MIN_SPEED, OneDimensionConstants.MAX_SPEED ) + // @public used by the time control to select a speed + this.timeSpeedProperty = new EnumerationProperty( TimeSpeed, TimeSpeed.NORMAL, { + validValues: [ TimeSpeed.NORMAL, TimeSpeed.SLOW ] } ); + + // @private {DerivedProperty.} multiplier applied to dt + this.timeScaleProperty = new DerivedProperty( + [ this.timeSpeedProperty ], + timeSpeed => ( timeSpeed === TimeSpeed.NORMAL ) ? NormalModesConstants.NORMAL_TIME_SCALE : NormalModesConstants.SLOW_TIME_SCALE + ); // @public {Property.} determines visibility of the phases sliders this.phasesVisibleProperty = new BooleanProperty( false, { @@ -209,7 +215,7 @@ reset() { this.playingProperty.reset(); this.timeProperty.reset(); - this.simSpeedProperty.reset(); + this.timeSpeedProperty.reset(); this.phasesVisibleProperty.reset(); this.springsVisibleProperty.reset(); this.numberVisibleMassesProperty.reset(); @@ -275,7 +281,7 @@ * @public */ singleStep( dt ) { - dt *= this.simSpeedProperty.get(); + dt *= this.timeScaleProperty.get(); this.timeProperty.set( this.timeProperty.get() + dt ); if ( this.draggingMassIndexProperty.get() > 0 ) { this.setVerletPositions( dt ); Index: js/common/view/NormalModesControlPanel.js IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/view/NormalModesControlPanel.js b/js/common/view/NormalModesControlPanel.js --- a/js/common/view/NormalModesControlPanel.js (revision f77813581e4211dcc43b6079c218a47cf1d5dfb5) +++ b/js/common/view/NormalModesControlPanel.js (date 1613088309341) @@ -18,6 +18,7 @@ import PlayPauseButton from '../../../../scenery-phet/js/buttons/PlayPauseButton.js'; import StepForwardButton from '../../../../scenery-phet/js/buttons/StepForwardButton.js'; import NumberControl from '../../../../scenery-phet/js/NumberControl.js'; +import TimeControlNode from '../../../../scenery-phet/js/TimeControlNode.js'; import HBox from '../../../../scenery/js/nodes/HBox.js'; import Text from '../../../../scenery/js/nodes/Text.js'; import VBox from '../../../../scenery/js/nodes/VBox.js'; @@ -32,14 +33,10 @@ import NormalModesColors from '../NormalModesColors.js'; import NormalModesConstants from '../NormalModesConstants.js'; -const fastString = normalModesStrings.fast; const initialPositionsString = normalModesStrings.initialPositions; -const normalString = normalModesStrings.normal; const numberOfMassesString = normalModesStrings.numberOfMasses; const showPhasesString = normalModesStrings.showPhases; const showSpringsString = normalModesStrings.showSprings; -const slowString = normalModesStrings.slow; -const speedString = normalModesStrings.speed; const zeroPositionsString = normalModesStrings.zeroPositions; class NormalModesControlPanel extends Panel { @@ -90,42 +87,6 @@ } ); } - const playPauseButtonOptions = { - upFill: NormalModesColors.BLUE_BUTTON_UP_COLOR, - overFill: NormalModesColors.BLUE_BUTTON_OVER_COLOR, - disabledFill: NormalModesColors.BLUE_BUTTON_DISABLED_COLOR, - downFill: NormalModesColors.BLUE_BUTTON_DOWN_COLOR, - backgroundGradientColorStop0: NormalModesColors.BLUE_BUTTON_BORDER_0, - backgroundGradientColorStop1: NormalModesColors.BLUE_BUTTON_BORDER_1, - innerButtonLineWidth: 1 - }; - - // TODO https://github.com/phetsims/normal-modes/issues/38 magic numbers - const playPauseButton = new PlayPauseButton( model.playingProperty, { - radius: 18, - scaleFactorWhenPaused: 1.15, - touchAreaDilation: 18, - pauseOptions: playPauseButtonOptions, - playOptions: playPauseButtonOptions - } ); - - const stepButton = new StepForwardButton( { - radius: 18, - touchAreaDilation: 15, - isPlayingProperty: model.playingProperty, - listener: () => { model.singleStep( OneDimensionConstants.FIXED_DT ); } - } ); - - const playAndStepButtons = new HBox( { - spacing: 7, - align: 'center', - children: [ - playPauseButton, - new VStrut( playPauseButton.height * 1.15 ), // to avoid resizing the HBox - stepButton - ] - } ); - // TODO https://github.com/phetsims/normal-modes/issues/38 magic numbers const textButtonsOptions = merge( { font: NormalModesConstants.GENERAL_FONT, @@ -189,53 +150,8 @@ }; }; - const speedControlOptions = { - delta: OneDimensionConstants.DELTA_SPEED, - layoutFunction: createLayoutFunction(), - includeArrowButtons: false, - sliderOptions: { - trackSize: new Dimension2( 150, 3 ), - thumbSize: new Dimension2( 11, 19 ), - thumbTouchAreaXDilation: 12, - thumbTouchAreaYDilation: 15, - majorTickLength: 10, - minorTickLength: 5, - majorTicks: [ - { - value: OneDimensionConstants.MIN_SPEED, - label: new Text( slowString, { font: NormalModesConstants.REALLY_SMALL_FONT } ) - }, - { - value: OneDimensionConstants.INIT_SPEED, - label: new Text( normalString, { font: NormalModesConstants.REALLY_SMALL_FONT } ) - }, - { - value: OneDimensionConstants.MAX_SPEED, - label: new Text( fastString, { font: NormalModesConstants.REALLY_SMALL_FONT } ) - } - ], - minorTickSpacing: OneDimensionConstants.DELTA_SPEED - }, - titleNodeOptions: { - font: NormalModesConstants.GENERAL_FONT - }, - numberDisplayOptions: { - visible: false - } - }; - - const speedControl = new NumberControl( - speedString, - model.simSpeedProperty, - new RangeWithValue( OneDimensionConstants.MIN_SPEED, - OneDimensionConstants.MAX_SPEED, - OneDimensionConstants.INIT_SPEED ), - speedControlOptions - ); - - const playSpeedControlsBox = new VBox( { - align: 'center', - children: [ playAndStepButtons, speedControl ] + const timeControlNode = new TimeControlNode( model.playingProperty, { + timeSpeedProperty: model.timeSpeedProperty } ); const numberVisibleMassesControlOptions = { @@ -277,7 +193,7 @@ spacing: 7, align: 'center', children: [ - playSpeedControlsBox, + timeControlNode, initialPositionsButton, zeroPositionsButton, numberVisibleMassesControl, ```

And here's a screenshot showing the TimeControlNode in the One Dimension screen:

screenshot_863
issamali67 commented 3 years ago

I think the difference between what @pixelzoom did and what I did is that @pixelzoom took out this.simSpeedProperty and defined another thisTimeScaleProperty . I tried instead to do the changes Chris suggested earlier in this.simSpeedProperty itself. And this did not work which puzzled me. I tried to find if this.simSpeedProperty is used somewhere else, but could no. So I am still puzzled!

jessegreenberg commented 3 years ago

The API for StepButton changed and isPlayingProperty was replaced with enabledProperty. When TimeControlNode is used in this sim the setting of enabledProperty can be removed because it is the default behavior for TimeControlNode.

pixelzoom commented 3 years ago

For https://github.com/phetsims/qa/issues/724, @ariel-phet requested this issue for the Prototype.

pixelzoom commented 3 years ago

This is ready for review in master by @DianaTavares and @ariel-phet. Screenshots are shown below.

@DianaTavares the mockups that you provided in https://github.com/phetsims/normal-modes/issues/58#issuecomment-776191959 do not match the layout of the control panel in the sim, and are missing UI components. And in one mockup, you show the time control in a separate panel, which is not possible because the 2 screens share the same control panel. So I simply moved the time control to the bottom of the control panel, with a separator above it. All other controls are where they were when I started this work.

@DianaTavares and @ariel-phet Let me know if we need to adjust the "slow" speed. It currently scales to 20% of normal speed.

screenshot_1388 screenshot_1389
ariel-phet commented 3 years ago

@pixelzoom I reviewed on master -- looks great, nice placement and certainly cleans things up and brings the sim to the modern standard. The slow speed seems appropriate. Everything now looks buttoned up for the porotype. Closing.