From 151018bfc266f45e3369629ec652b1930bc4b821 Mon Sep 17 00:00:00 2001 From: Jesse Date: Thu, 5 Dec 2024 19:17:56 -0500 Subject: [PATCH] drag sounds for the angle controls, see #105 --- js/trig-tour/TrigTourQueryParameters.ts | 25 ++++++++++++++++++ js/trig-tour/model/TrigTourModel.ts | 15 ++++++++--- js/trig-tour/view/AngleSoundGenerator.ts | 33 ++++++++++++++++++++++++ js/trig-tour/view/GraphView.ts | 17 +++++++++++- js/trig-tour/view/TrigTourScreenView.ts | 8 ++++-- js/trig-tour/view/UnitCircleView.ts | 17 +++++++++++- 6 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 js/trig-tour/TrigTourQueryParameters.ts create mode 100644 js/trig-tour/view/AngleSoundGenerator.ts diff --git a/js/trig-tour/TrigTourQueryParameters.ts b/js/trig-tour/TrigTourQueryParameters.ts new file mode 100644 index 0000000..1772c68 --- /dev/null +++ b/js/trig-tour/TrigTourQueryParameters.ts @@ -0,0 +1,25 @@ +// Copyright 2024, University of Colorado Boulder + +/** + * Query parameters for this simulation. + * + * @author Jesse Greenberg (PhET Interactive Simulations) + */ + +import trigTour from '../trigTour.js'; + +const TrigTourQueryParameters = QueryStringMachine.getAll( { + + // Reduces the number of rotations in the unit circle so that it is easier to find the limit.s + maxRotations: { + type: 'number', + + // Value must be an integer becase it is used in a multiple of PI. It must be an even + // number because the drag handlers in this sim assume that in their calculations. + isValidValue: ( value: number ) => value < 60 && Number.isInteger( value ) && value % 2 === 0, + defaultValue: 50 // High number makes it difficult and whimsical to find the limit. + } +} ); + +trigTour.register( 'TrigTourQueryParameters', TrigTourQueryParameters ); +export default TrigTourQueryParameters; \ No newline at end of file diff --git a/js/trig-tour/model/TrigTourModel.ts b/js/trig-tour/model/TrigTourModel.ts index 0c3c8d6..9947f13 100644 --- a/js/trig-tour/model/TrigTourModel.ts +++ b/js/trig-tour/model/TrigTourModel.ts @@ -14,10 +14,13 @@ import Vector2 from '../../../../dot/js/Vector2.js'; import trigTour from '../../trigTour.js'; import SpecialAngles from '../SpecialAngles.js'; import TrigTourConstants from '../TrigTourConstants.js'; +import TrigTourQueryParameters from '../TrigTourQueryParameters.js'; // constants const MAX_SMALL_ANGLE_LIMIT = 0.5 * Math.PI; -const MAX_ANGLE_LIMIT = 50 * Math.PI + MAX_SMALL_ANGLE_LIMIT; // must be ( integer+0.5) number of full rotations + +// must be ( integer+0.5) number of full rotations +const MAX_ANGLE_LIMIT = TrigTourQueryParameters.maxRotations * Math.PI + MAX_SMALL_ANGLE_LIMIT; class TrigTourModel { @@ -163,7 +166,11 @@ class TrigTourModel { this.smallAngle = fullAngleInRads - this.rotationNumberFromPi * 2 * Math.PI; remainderAngle = fullAngleInRads % ( Math.PI ); this._halfTurnCount = Utils.roundSymmetric( ( fullAngleInRads - remainderAngle ) / ( Math.PI ) ); - this.fullAngleProperty.value = fullAngleInRads; + this.fullAngleProperty.value = this.constrainFullAngle( fullAngleInRads ); + } + + private constrainFullAngle( fullAngle: number ): number { + return Utils.clamp( fullAngle, -MAX_ANGLE_LIMIT, MAX_ANGLE_LIMIT ); } /** @@ -198,7 +205,7 @@ class TrigTourModel { this._fullTurnCount = Utils.roundSymmetric( ( targetAngle - remainderAngle ) / ( 2 * Math.PI ) ); remainderAngle = targetAngle % ( Math.PI ); this._halfTurnCount = Utils.roundSymmetric( ( targetAngle - remainderAngle ) / ( Math.PI ) ); - this.fullAngleProperty.value = targetAngle; // now can trigger angle update + this.fullAngleProperty.value = this.constrainFullAngle( targetAngle ); // now can trigger angle update this.previousAngle = smallAngle; } @@ -324,7 +331,7 @@ class TrigTourModel { public checkMaxAngleExceeded(): void { // determine if max angle is exceeded and set the property. - this.maxAngleExceededProperty.value = ( Math.abs( this.getFullAngleInRadians() ) > MAX_ANGLE_LIMIT ); + this.maxAngleExceededProperty.value = ( Math.abs( this.getFullAngleInRadians() ) >= MAX_ANGLE_LIMIT ); } public static readonly MAX_SMALL_ANGLE_LIMIT = MAX_SMALL_ANGLE_LIMIT; diff --git a/js/trig-tour/view/AngleSoundGenerator.ts b/js/trig-tour/view/AngleSoundGenerator.ts new file mode 100644 index 0000000..4d3064e --- /dev/null +++ b/js/trig-tour/view/AngleSoundGenerator.ts @@ -0,0 +1,33 @@ +// Copyright 2024, University of Colorado Boulder + +/** + * AngleSoundGenerator is a sound generator specifically designed to produce sounds for the + * controls that change the angle in trig-tour. + * + * @author Jesse Greenberg (PhET Interactive Simulations) + */ + +import Range from '../../../../dot/js/Range.js'; +import Utils from '../../../../dot/js/Utils.js'; +import ValueChangeSoundPlayer from '../../../../tambo/js/sound-generators/ValueChangeSoundPlayer.js'; +import trigTour from '../../trigTour.js'; +import TrigTourModel from '../model/TrigTourModel.js'; + +class AngleSoundGenerator extends ValueChangeSoundPlayer { + public constructor() { + + const range = new Range( -TrigTourModel.MAX_ANGLE_LIMIT, TrigTourModel.MAX_ANGLE_LIMIT ); + super( range, { + + // Limit precision so that comparison against the range limits works consistently. + constrainValue: ( value: number ) => Utils.toFixedNumber( value, 1 ), + + // Arbitrary, but creates a consistent sound as the user interacts with the angle. + numberOfMiddleThresholds: 700 + } ); + } +} + +trigTour.register( 'AngleSoundGenerator', AngleSoundGenerator ); + +export default AngleSoundGenerator; \ No newline at end of file diff --git a/js/trig-tour/view/GraphView.ts b/js/trig-tour/view/GraphView.ts index 2157732..192f172 100644 --- a/js/trig-tour/view/GraphView.ts +++ b/js/trig-tour/view/GraphView.ts @@ -26,6 +26,7 @@ import trigTour from '../../trigTour.js'; import TrigTourStrings from '../../TrigTourStrings.js'; import TrigTourModel from '../model/TrigTourModel.js'; import TrigTourConstants from '../TrigTourConstants.js'; +import AngleSoundGenerator from './AngleSoundGenerator.js'; import TrigFunctionLabelText from './TrigFunctionLabelText.js'; import TrigIndicatorArrowNode from './TrigIndicatorArrowNode.js'; import TrigPlotsNode from './TrigPlotsNode.js'; @@ -87,8 +88,9 @@ class GraphView extends Node { * @param height of y-axis on graph * @param width of x-axis on graph * @param viewProperties + * @param angleSoundGenerator */ - public constructor( trigTourModel: TrigTourModel, height: number, width: number, viewProperties: ViewProperties ) { + public constructor( trigTourModel: TrigTourModel, height: number, width: number, viewProperties: ViewProperties, angleSoundGenerator: AngleSoundGenerator ) { super(); this.trigTourModel = trigTourModel; @@ -255,6 +257,8 @@ class GraphView extends Node { // make sure the full angle does not exceed max allowed angle trigTourModel.checkMaxAngleExceeded(); + const oldValue = trigTourModel.getFullAngleInRadians(); + // For alt input, use modelDelta to increment/decrement the full angle if ( event.isFromPDOM() ) { fullAngle = trigTourModel.getNextFullDeltaFromKeyboardInput( listener.modelDelta, viewProperties.specialAnglesVisibleProperty.value ); @@ -267,6 +271,17 @@ class GraphView extends Node { } trigTourModel.setNewFullAngle( fullAngle, viewProperties.specialAnglesVisibleProperty.value ); + + // After the new value has been computed, play the sound if the value has changed + const newValue = trigTourModel.getFullAngleInRadians(); + if ( oldValue !== newValue ) { + if ( event.isFromPDOM() ) { + angleSoundGenerator.playSoundForValueChange( newValue, oldValue ); + } + else { + angleSoundGenerator.playSoundIfThresholdReached( newValue, oldValue ); + } + } } } ); diff --git a/js/trig-tour/view/TrigTourScreenView.ts b/js/trig-tour/view/TrigTourScreenView.ts index cb340b7..ff90b01 100644 --- a/js/trig-tour/view/TrigTourScreenView.ts +++ b/js/trig-tour/view/TrigTourScreenView.ts @@ -17,6 +17,7 @@ import dizzyPhetGirl_png from '../../../mipmaps/dizzyPhetGirl_png.js'; import trigTour from '../../trigTour.js'; import TrigTourStrings from '../../TrigTourStrings.js'; import TrigTourModel from '../model/TrigTourModel.js'; +import AngleSoundGenerator from './AngleSoundGenerator.js'; import ControlPanel from './ControlPanel.js'; import GraphView from './GraphView.js'; import ValuesAccordionBox from './readout/ValuesAccordionBox.js'; @@ -59,10 +60,13 @@ class TrigTourScreenView extends ScreenView { whiteSheet.x = this.layoutBounds.centerX; whiteSheet.top = this.layoutBounds.top + 20; - const unitCircleView = new UnitCircleView( trigTourModel, whiteSheet, xOffset, viewProperties ); + // A reusable sound generator for the UI components that can control the angle. + const angleSoundGenerator = new AngleSoundGenerator(); + + const unitCircleView = new UnitCircleView( trigTourModel, whiteSheet, xOffset, viewProperties, angleSoundGenerator ); unitCircleView.center = whiteSheet.center; - const graphView = new GraphView( trigTourModel, 0.25 * this.layoutBounds.height, 0.92 * this.layoutBounds.width, viewProperties ); + const graphView = new GraphView( trigTourModel, 0.25 * this.layoutBounds.height, 0.92 * this.layoutBounds.width, viewProperties, angleSoundGenerator ); graphView.x = this.layoutBounds.centerX; graphView.y = this.layoutBounds.bottom - graphView.graphAxesNode.bottom - 15; diff --git a/js/trig-tour/view/UnitCircleView.ts b/js/trig-tour/view/UnitCircleView.ts index f42317b..fef9ecf 100644 --- a/js/trig-tour/view/UnitCircleView.ts +++ b/js/trig-tour/view/UnitCircleView.ts @@ -20,6 +20,7 @@ import TrigTourModel from '../model/TrigTourModel.js'; import SpecialAngles from '../SpecialAngles.js'; import TrigTourConstants from '../TrigTourConstants.js'; import TrigTourMathStrings from '../TrigTourMathStrings.js'; +import AngleSoundGenerator from './AngleSoundGenerator.js'; import TrigIndicatorArrowNode from './TrigIndicatorArrowNode.js'; import TrigTourColors from './TrigTourColors.js'; import TrigTourSpiralNode from './TrigTourSpiralNode.js'; @@ -55,8 +56,9 @@ class UnitCircleView extends Node { * @param viewRectangle - Rectangle for the background rectangle of the unit circle, including view properties like lineWidth * @param backgroundOffset - Offset of the background rectangle behind the unit circle view * @param viewProperties - collection of properties handling visibility of elements on screen + * @param angleSoundGenerator - sound generator for angle changes */ - public constructor( trigTourModel: TrigTourModel, viewRectangle: Rectangle, backgroundOffset: number, viewProperties: ViewProperties ) { + public constructor( trigTourModel: TrigTourModel, viewRectangle: Rectangle, backgroundOffset: number, viewProperties: ViewProperties, angleSoundGenerator: AngleSoundGenerator ) { super(); // Draw Unit Circle @@ -190,6 +192,8 @@ class UnitCircleView extends Node { // make sure the full angle does not exceed max allowed angle trigTourModel.checkMaxAngleExceeded(); + const oldValue = trigTourModel.getFullAngleInRadians(); + if ( event.isFromPDOM() ) { const newFullAngle = trigTourModel.getNextFullDeltaFromKeyboardInput( listener.modelDelta, viewProperties.specialAnglesVisibleProperty.value ); trigTourModel.setNewFullAngle( newFullAngle, viewProperties.specialAnglesVisibleProperty.value ); @@ -222,6 +226,17 @@ class UnitCircleView extends Node { } } } + + // After the new value has been computed, play the sound if the value has changed + const newValue = trigTourModel.getFullAngleInRadians(); + if ( oldValue !== newValue ) { + if ( event.isFromPDOM() ) { + angleSoundGenerator.playSoundForValueChange( newValue, oldValue ); + } + else { + angleSoundGenerator.playSoundIfThresholdReached( newValue, oldValue ); + } + } } } ) );