define( function( require ) { 'use strict'; // modules var inherit = require( 'PHET_CORE/inherit' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var RectangularPushButton = require( 'SUN/buttons/RectangularPushButton' ); var StopSignNode = require( 'SCENERY_PHET/StopSignNode' ); /** * @constructor * * @param {Object} [options] */ function StopButton( options ) { RectangularPushButton.call( this, _.extend( { xMargin: 7, yMargin: 3, touchAreaXDilation: 6, touchAreaYDilation: 6, baseColor: 'rgb( 231, 232, 233 )', content: new StopSignNode( { scale: 0.4 } ) }, options ) ); } pendulumLab.register( 'StopButton', StopButton ); return inherit( RectangularPushButton, StopButton ); } );
define( function( require ) { 'use strict'; // modules var EnergyBox = require( 'PENDULUM_LAB/energy/view/EnergyBox' ); var inherit = require( 'PHET_CORE/inherit' ); var NumberProperty = require( 'AXON/NumberProperty' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' ); var PendulumLabScreenView = require( 'PENDULUM_LAB/common/view/PendulumLabScreenView' ); /** * @constructor * * @param {PendulumLabModel} model */ function EnergyScreenView( model, options ) { PendulumLabScreenView.call( this, model, options ); // @protected {Property.<number>} this.chartHeightProperty = new NumberProperty( 200 ); // create and add energy graph node to the bottom layer var energyGraphNode = new EnergyBox( model, this.chartHeightProperty, { left: this.layoutBounds.left + PendulumLabConstants.PANEL_PADDING, top: this.layoutBounds.top + PendulumLabConstants.PANEL_PADDING } ); this.energyGraphLayer.addChild( energyGraphNode ); // @protected {EnergyBox} this.energyGraphNode = energyGraphNode; // move ruler and stopwatch to the right side this.rulerNode.centerX += ( energyGraphNode.width + 10 ); model.ruler.setInitialLocationValue( this.rulerNode.center ); this.stopwatchNode.left = this.rulerNode.right + 10; model.stopwatch.setInitialLocationValue( this.stopwatchNode.center ); this.resizeEnergyGraphToFit(); } pendulumLab.register( 'EnergyScreenView', EnergyScreenView ); return inherit( PendulumLabScreenView, EnergyScreenView, { /** * Changes the chart height so that the energy graph fits all available size * @protected */ resizeEnergyGraphToFit: function() { var currentSpace = this.toolsControlPanelNode.top - this.energyGraphNode.bottom; var desiredSpace = PendulumLabConstants.PANEL_PADDING; this.chartHeightProperty.value += currentSpace - desiredSpace; } } ); } );
define( function( require ) { 'use strict'; // modules var BooleanProperty = require( 'AXON/BooleanProperty' ); var inherit = require( 'PHET_CORE/inherit' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var Property = require( 'AXON/Property' ); /** * @constructor * * @param {boolean} isInitiallyVisible */ function MovableComponent( isInitiallyVisible ) { // @public {Property.<Vector2|null>} - Initial value will be set in view, after calculating all bounds of nodes this.locationProperty = new Property( null ); // @public {Property.<boolean>} flag to determine stopwatch state this.isVisibleProperty = new BooleanProperty( isInitiallyVisible ); } pendulumLab.register( 'MovableComponent', MovableComponent ); return inherit( Object, MovableComponent, { /** * Function that sets the initial location of a movable object and keeps an internal copy of it. * @public * * @param {Vector2} initialLocation */ setInitialLocationValue: function( initialLocation ) { // position to use for resetting // make a copy of the initial location vector this.initialLocation = initialLocation.copy(); // set the location to the initial location this.locationProperty.value = this.initialLocation.copy(); }, /** * Reset function * @public */ reset: function() { // Reset the location to the initial location this.locationProperty.value = this.initialLocation ? this.initialLocation.copy() : null; this.isVisibleProperty.reset(); } } ); } );
define( function( require ) { 'use strict'; // modules var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); // strings var customString = require( 'string!PENDULUM_LAB/custom' ); var earthString = require( 'string!PENDULUM_LAB/earth' ); var jupiterString = require( 'string!PENDULUM_LAB/jupiter' ); var moonString = require( 'string!PENDULUM_LAB/moon' ); var planetXString = require( 'string!PENDULUM_LAB/planetX' ); /** * @constructor * * @param {string} title * @param {number|null} gravity - Gravitational acceleration on body (m/s^2) if defined. */ function Body( title, gravity ) { // @public {string} (read-only) this.title = title; // @public {number|null} (read-only) - Gravitation acceleration (if available) in meters/second^2 this.gravity = gravity; } pendulumLab.register( 'Body', Body ); Body.MOON = new Body( moonString, 1.62 ); Body.EARTH = new Body( earthString, 9.81 ); Body.JUPITER = new Body( jupiterString, 24.79 ); Body.PLANET_X = new Body( planetXString, 14.2 ); Body.CUSTOM = new Body( customString, null ); // array of all the bodies used in the simulation. Body.BODIES = [ Body.MOON, Body.EARTH, Body.JUPITER, Body.PLANET_X, Body.CUSTOM ]; // verify that enumeration is immutable, without the runtime penalty in production code if ( assert ) { Object.freeze( Body ); } return Body; } );
define( function( require ) { 'use strict'; // modules var Body = require( 'PENDULUM_LAB/common/model/Body' ); var BooleanProperty = require( 'AXON/BooleanProperty' ); var inherit = require( 'PHET_CORE/inherit' ); var NumberProperty = require( 'AXON/NumberProperty' ); var Pendulum = require( 'PENDULUM_LAB/common/model/Pendulum' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var Property = require( 'AXON/Property' ); var RangeWithValue = require( 'DOT/RangeWithValue' ); var Ruler = require( 'PENDULUM_LAB/common/model/Ruler' ); var Stopwatch = require( 'PENDULUM_LAB/common/model/Stopwatch' ); /** * @constructor * * @param {Object} [options] */ function PendulumLabModel( options ) { var self = this; options = _.extend( { // {boolean} - Should be true if there is a PeriodTimer handling the trace's visibility. hasPeriodTimer: false, // {boolean} rulerInitiallyVisible: true }, options ); // @public {Property.<Body>} this.bodyProperty = new Property( Body.EARTH ); // @public {Property.<number>} - Gravitational acceleration this.gravityProperty = new NumberProperty( Body.EARTH.gravity ); // @public {Property.<number>} - Tracked for the "Custom" body, so that we can revert to this when the user changes // from "Planet X" to "Custom" this.customGravityProperty = new NumberProperty( Body.EARTH.gravity ); // @public {Property.<number>} - Speed of time. this.timeSpeedProperty = new NumberProperty( 1 ); // @public {Property.<number>} - Number of visible pendula (2 pendula are handled in the model) this.numberOfPendulaProperty = new NumberProperty( 1 ); // @public {Property.<boolean>} this.isPlayingProperty = new BooleanProperty( true ); // @public {Property.<number>} - Friction coefficient this.frictionProperty = new NumberProperty( 0 ); // @public {Property.<boolean} this.isPeriodTraceVisibleProperty = new BooleanProperty( false ); // @public {Property.<number>} this.energyZoomProperty = new NumberProperty( 1 ); // @public {Array.<Pendulum>} this.pendula = [ new Pendulum( 0, 1, 0.7, true, this.gravityProperty, this.frictionProperty, this.isPeriodTraceVisibleProperty, options.hasPeriodTimer ), new Pendulum( 1, 0.5, 1.0, false, this.gravityProperty, this.frictionProperty, this.isPeriodTraceVisibleProperty, options.hasPeriodTimer ) ]; // @public (read-only) possible gravity range 0m/s^2 to 25m/s^2 this.gravityRange = new RangeWithValue( 0, 25, this.gravityProperty.value ); // @public (read-only) possible friction range this.frictionRange = new RangeWithValue( 0, 0.5115, 0 ); // @public (read-only) model for ruler this.ruler = new Ruler( options.rulerInitiallyVisible ); // @public (read-only) model for stopwatch this.stopwatch = new Stopwatch( false ); // change gravity if body was changed this.bodyProperty.lazyLink( function( body, oldBody ) { // If it's not custom, set it to its value if ( body !== Body.CUSTOM ) { self.gravityProperty.value = body.gravity; } else { // If we are switching from Planet X to Custom, don't let them cheat (go back to last custom value) if ( oldBody === Body.PLANET_X ) { self.gravityProperty.value = self.customGravityProperty.value; } // For non-Planet X, update our internal custom gravity else { self.customGravityProperty.value = self.gravityProperty.value; } } } ); // change body to custom if gravity was changed this.gravityProperty.lazyLink( function( gravity ) { if ( !_.some( Body.BODIES, function( body ) { return body.gravity === gravity; } ) ) { self.bodyProperty.value = Body.CUSTOM; } if ( self.bodyProperty.value === Body.CUSTOM ) { self.customGravityProperty.value = gravity; } } ); // change pendulum visibility if number of pendula was changed this.numberOfPendulaProperty.link( function( numberOfPendula ) { self.pendula.forEach( function( pendulum, pendulumIndex ) { pendulum.isVisibleProperty.value = ( numberOfPendula > pendulumIndex ); } ); } ); } pendulumLab.register( 'PendulumLabModel', PendulumLabModel ); return inherit( Object, PendulumLabModel, { /** * Resets the model. * @public */ reset: function() { this.bodyProperty.reset(); this.gravityProperty.reset(); this.customGravityProperty.reset(); this.timeSpeedProperty.reset(); this.numberOfPendulaProperty.reset(); this.isPlayingProperty.reset(); this.frictionProperty.reset(); this.isPeriodTraceVisibleProperty.reset(); this.energyZoomProperty.reset(); // reset ruler model this.ruler.reset(); // reset stopwatch model this.stopwatch.reset(); // reset pendulum models this.pendula.forEach( function( pendulum ) { pendulum.reset(); } ); }, /** * Steps the model forward in time. * @public * * @param {number} dt */ step: function( dt ) { if ( this.isPlayingProperty.value ) { // pick a number as irrational (in the mathematical sense) as possible so that the last digits on the period timer do get stuck to a number var periodTimerOffsetFactor = 1.007; // For our accuracy guarantees, we cap our DT fairly low. Otherwise the fixed-step model may become inaccurate // enough for getting an accurate period timer or speed loss on Jupiter with the shortest length. // We apply this BEFORE speed is applied, so that even if we're on a slow device, slow-motion WILL be guaranteed // to slow the sim speed down. this.modelStep( Math.min( 0.05, dt ) * ( this.timeSpeedProperty.value * periodTimerOffsetFactor ) ); } }, /** * Steps in model time. * @private * * @param {number} dt - change in time measured in seconds */ modelStep: function( dt ) { // add time to the stopwatch if it is running if ( this.stopwatch.isRunningProperty.value ) { this.stopwatch.elapsedTimeProperty.value += dt; } // loop over the pendula for ( var i = 0; i < this.numberOfPendulaProperty.value; i++ ) { var pendulum = this.pendula[ i ]; // get the pendulum from the array // if the pendulum is moving if ( !pendulum.isStationary() ) { // prevent infinite motion after friction. var dampMotion = ( Math.abs( pendulum.angleProperty.value ) < 1e-3 ) && ( Math.abs( pendulum.angularAccelerationProperty.value ) < 1e-3 ) && ( Math.abs( pendulum.angularVelocityProperty.value ) < 1e-3 ); if ( dampMotion ) { pendulum.angleProperty.value = 0; pendulum.angularVelocityProperty.value = 0; } // step through the pendulum model pendulum.step( dt ); } } }, /** * Steps forward by a specific amount of time (even if paused). * @public */ stepManual: function() { this.modelStep( 0.01 ); // advances by 10 ms, see https://github.com/phetsims/pendulum-lab/issues/182 }, /** * Returns the pendula to rest. * @public */ returnPendula: function() { //reset the pendula this.pendula.forEach( function( pendulum ) { pendulum.resetThermalEnergy(); pendulum.resetMotion(); } ); // stop the timer if ( this.periodTimer ) { this.periodTimer.stop(); } } } ); } );
define( function( require ) { 'use strict'; // modules var AlignBox = require( 'SCENERY/nodes/AlignBox' ); var AlignGroup = require( 'SCENERY/nodes/AlignGroup' ); var Dialog = require( 'SUN/Dialog' ); var HBox = require( 'SCENERY/nodes/HBox' ); var inherit = require( 'PHET_CORE/inherit' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' ); var RichText = require( 'SCENERY/nodes/RichText' ); var Text = require( 'SCENERY/nodes/Text' ); var VBox = require( 'SCENERY/nodes/VBox' ); // strings var energyLegendString = require( 'string!PENDULUM_LAB/energyLegend' ); var legendKineticEnergyAbbreviationString = require( 'string!PENDULUM_LAB/legend.kineticEnergyAbbreviation' ); var legendKineticEnergyString = require( 'string!PENDULUM_LAB/legend.kineticEnergy' ); var legendPotentialEnergyAbbreviationString = require( 'string!PENDULUM_LAB/legend.potentialEnergyAbbreviation' ); var legendPotentialEnergyString = require( 'string!PENDULUM_LAB/legend.potentialEnergy' ); var legendThermalEnergyAbbreviationString = require( 'string!PENDULUM_LAB/legend.thermalEnergyAbbreviation' ); var legendThermalEnergyString = require( 'string!PENDULUM_LAB/legend.thermalEnergy' ); var legendTotalEnergyAbbreviationString = require( 'string!PENDULUM_LAB/legend.totalEnergyAbbreviation' ); var legendTotalEnergyString = require( 'string!PENDULUM_LAB/legend.totalEnergy' ); /** * @constructor */ function EnergyLegendDialog() { var abbreviationGroup = new AlignGroup(); var descriptionGroup = new AlignGroup(); var content = new VBox( { spacing: 15, children: [ { abbreviation: legendKineticEnergyAbbreviationString, description: legendKineticEnergyString, color: PendulumLabConstants.KINETIC_ENERGY_COLOR }, { abbreviation: legendPotentialEnergyAbbreviationString, description: legendPotentialEnergyString, color: PendulumLabConstants.POTENTIAL_ENERGY_COLOR }, { abbreviation: legendThermalEnergyAbbreviationString, description: legendThermalEnergyString, color: PendulumLabConstants.THERMAL_ENERGY_COLOR }, { abbreviation: legendTotalEnergyAbbreviationString, description: legendTotalEnergyString, color: PendulumLabConstants.TOTAL_ENERGY_COLOR } ].map( function( itemData ) { return new HBox( { spacing: 20, children: [ new AlignBox( new RichText( itemData.abbreviation, { font: PendulumLabConstants.LEGEND_ABBREVIATION_FONT, fill: itemData.color, maxWidth: 100 } ), { group: abbreviationGroup, xAlign: 'left' } ), new AlignBox( new Text( itemData.description, { font: PendulumLabConstants.LEGEND_DESCRIPTION_FONT } ), { group: descriptionGroup, xAlign: 'left', maxWidth: 500 } ) ] } ); } ) } ); Dialog.call( this, content, { ySpacing: 20, title: new Text( energyLegendString, { font: PendulumLabConstants.DIALOG_TITLE_FONT, maxWidth: 700 } ) } ); } pendulumLab.register( 'EnergyLegendDialog', EnergyLegendDialog ); return inherit( Dialog, EnergyLegendDialog ); } );
define( function( require ) { 'use strict'; // modules var ArrowNode = require( 'SCENERY_PHET/ArrowNode' ); var Bounds2 = require( 'DOT/Bounds2' ); var Color = require( 'SCENERY/util/Color' ); var Dimension2 = require( 'DOT/Dimension2' ); var inherit = require( 'PHET_CORE/inherit' ); var Line = require( 'SCENERY/nodes/Line' ); var LinearGradient = require( 'SCENERY/util/LinearGradient' ); var Node = require( 'SCENERY/nodes/Node' ); var Pendulum = require( 'PENDULUM_LAB/common/model/Pendulum' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' ); var Property = require( 'AXON/Property' ); var Rectangle = require( 'SCENERY/nodes/Rectangle' ); var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' ); var Text = require( 'SCENERY/nodes/Text' ); var Util = require( 'DOT/Util' ); var Vector2 = require( 'DOT/Vector2' ); // constants var ARROW_HEAD_WIDTH = 12; var ARROW_TAIL_WIDTH = 6; var ARROW_SIZE_DEFAULT = 25; var RECT_SIZE = new Dimension2( 73, 98 ); /** * @constructor * * @param {Array.<Pendulum>} pendula - Array of pendulum models. * @param {ModelViewTransform2} modelViewTransform * @param {Object} [options] */ function PendulaNode( pendula, modelViewTransform, options ) { var self = this; options = _.extend( { preventFit: true }, options ); Node.call( this, options ); var viewOriginPosition = modelViewTransform.modelToViewPosition( Vector2.ZERO ); // @public {startDrag: {function}, computeDistance: {function}} - To identify how close a draggable object is. this.draggableItems = []; var pendulumNodes = []; var velocityArrows = []; var accelerationArrows = []; pendula.forEach( function( pendulum, pendulumIndex ) { var massToScale = function( mass ) { // height/width/depth of mass scale by cube-root to maintain density return 0.3 + 0.4 * Math.sqrt( mass / 1.5 ); }; // create the visual representation of a rod that joins the fulcrum point to the bob // initially set to be vertical var solidLine = new Line( 0, 0, 0, modelViewTransform.modelToViewDeltaY( pendulum.lengthProperty.value ), { stroke: 'black', pickable: false } ); // create the visual representation of a pendulum bob (a rectangle with a string and a line across the rectangle) var pendulumRect = new Node( { children: [ new Rectangle( -RECT_SIZE.width / 2, -RECT_SIZE.height / 2, RECT_SIZE.width, RECT_SIZE.height, { fill: new LinearGradient( -RECT_SIZE.width / 2, 0, RECT_SIZE.width / 2, 0 ).addColorStop( 0, Color.toColor( pendulum.color ).colorUtilsBrighter( 0.4 ) ) .addColorStop( 0.2, Color.toColor( pendulum.color ).colorUtilsBrighter( 0.9 ) ) .addColorStop( 0.7, pendulum.color ) } ), new Text( ( pendulumIndex + 1 ).toString(), { font: PendulumLabConstants.PENDULUM_LABEL_FONT, fill: 'white', centerY: RECT_SIZE.height / 4, centerX: 0, pickable: false } ), new Line( -RECT_SIZE.width / 2, 0, RECT_SIZE.width / 2, 0, { stroke: 'black', lineCap: 'butt', pickable: false } ) ] } ); // create the visual representation of a pendulum (bob + rod) var pendulumNode = new Node( { cursor: 'pointer', children: [ solidLine, pendulumRect ] } ); // add velocity arrows if necessary if ( options.isVelocityVisibleProperty ) { var velocityArrow = new ArrowNode( 0, 0, 0, 0, { pickable: false, fill: PendulumLabConstants.VELOCITY_ARROW_COLOR, tailWidth: ARROW_TAIL_WIDTH, headWidth: ARROW_HEAD_WIDTH } ); velocityArrows.push( velocityArrow ); // no need to unlink, present for the lifetime of the sim Property.multilink( [ pendulum.isVisibleProperty, options.isVelocityVisibleProperty, pendulum.velocityProperty ], function( pendulumVisible, velocityVisible, velocity ) { velocityArrow.visible = pendulumVisible && velocityVisible; // update the size of the arrow if ( velocityArrow.visible ) { var position = modelViewTransform.modelToViewPosition( pendulum.positionProperty.value ); velocityArrow.setTailAndTip( position.x, position.y, position.x + ARROW_SIZE_DEFAULT * velocity.x, position.y - ARROW_SIZE_DEFAULT * velocity.y ); } } ); } // add acceleration arrows if necessary if ( options.isAccelerationVisibleProperty ) { // create acceleration arrow var accelerationArrow = new ArrowNode( 0, 0, 0, 0, { pickable: false, fill: PendulumLabConstants.ACCELERATION_ARROW_COLOR, tailWidth: ARROW_TAIL_WIDTH, headWidth: ARROW_HEAD_WIDTH } ); accelerationArrows.push( accelerationArrow ); // no need to unlink, present for the lifetime of the sim Property.multilink( [ pendulum.isVisibleProperty, options.isAccelerationVisibleProperty, pendulum.accelerationProperty ], function( pendulumVisible, accelerationVisible, acceleration ) { accelerationArrow.visible = pendulumVisible && accelerationVisible; if ( accelerationArrow.visible ) { var position = modelViewTransform.modelToViewPosition( pendulum.positionProperty.value ); accelerationArrow.setTailAndTip( position.x, position.y, position.x + ARROW_SIZE_DEFAULT * acceleration.x, position.y - ARROW_SIZE_DEFAULT * acceleration.y ); } } ); } pendulumNodes.push( pendulumNode ); // add drag events var angleOffset; var dragListener = new SimpleDragHandler( { allowTouchSnag: true, // determine the position of where the pendulum is dragged. start: function( event ) { var dragAngle = modelViewTransform.viewToModelPosition( self.globalToLocalPoint( event.pointer.point ) ).angle + Math.PI / 2; angleOffset = pendulum.angleProperty.value - dragAngle; pendulum.isUserControlledProperty.value = true; }, // set the angle of the pendulum depending on where it is dragged to. drag: function( event ) { var dragAngle = modelViewTransform.viewToModelPosition( self.globalToLocalPoint( event.pointer.point ) ).angle + Math.PI / 2; var continuousAngle = Pendulum.modAngle( angleOffset + dragAngle ); // Round angles to nearest degree, see https://github.com/phetsims/pendulum-lab/issues/195 var roundedAngleDegrees = Util.roundSymmetric( Util.toDegrees( continuousAngle ) ); // Don't allow snapping to 180, see https://github.com/phetsims/pendulum-lab/issues/195 if ( Math.abs( roundedAngleDegrees ) === 180 ) { roundedAngleDegrees = Util.sign( roundedAngleDegrees ) * 179; } var roundedAngle = Util.toRadians( roundedAngleDegrees ); pendulum.angleProperty.value = roundedAngle; }, // release user control end: function() { pendulum.isUserControlledProperty.value = false; } } ); // add a drag listener pendulumRect.addInputListener( dragListener ); self.draggableItems.push( { startDrag: dragListener.startDrag.bind( dragListener ), computeDistance: function( globalPoint ) { if ( pendulum.isUserControlledProperty.value || !pendulum.isVisibleProperty.value ) { return Number.POSITIVE_INFINITY; } else { var cursorModelPosition = modelViewTransform.viewToModelPosition( self.globalToLocalPoint( globalPoint ) ); cursorModelPosition.rotate( -pendulum.angleProperty.value ).add( new Vector2( 0, pendulum.lengthProperty.value ) ); // rotate/length so (0,0) would be mass center var massViewWidth = modelViewTransform.viewToModelDeltaX( RECT_SIZE.width * massToScale( pendulum.massProperty.value ) ); var massViewHeight = modelViewTransform.viewToModelDeltaX( RECT_SIZE.height * massToScale( pendulum.massProperty.value ) ); var massBounds = new Bounds2( -massViewWidth / 2, -massViewHeight / 2, massViewWidth / 2, massViewHeight / 2 ); return Math.sqrt( massBounds.minimumDistanceToPointSquared( cursorModelPosition ) ); } } } ); // update pendulum rotation, pendulum.angleProperty.value is radians // we are using an inverted modelViewTransform, hence we multiply the view angle by minus one pendulum.angleProperty.link( function( angle ) { pendulumNode.rotation = -angle; pendulumNode.translation = viewOriginPosition; } ); // update pendulum components position pendulum.lengthProperty.link( function( length ) { var viewPendulumLength = modelViewTransform.modelToViewDeltaX( length ); pendulumRect.setY( viewPendulumLength ); solidLine.setY2( viewPendulumLength ); } ); // update rectangle size pendulum.massProperty.link( function( mass ) { pendulumRect.setScaleMagnitude( massToScale( mass ) ); } ); // update visibility pendulum.isVisibleProperty.linkAttribute( pendulumNode, 'visible' ); } ); this.children = pendulumNodes.concat( velocityArrows ).concat( accelerationArrows ); } pendulumLab.register( 'PendulaNode', PendulaNode ); return inherit( Node, PendulaNode ); } );
define( function( require ) { 'use strict'; // modules var ClosestDragListener = require( 'SUN/ClosestDragListener' ); var GlobalControlPanel = require( 'PENDULUM_LAB/common/view/GlobalControlPanel' ); var inherit = require( 'PHET_CORE/inherit' ); var Node = require( 'SCENERY/nodes/Node' ); var PendulaNode = require( 'PENDULUM_LAB/common/view/PendulaNode' ); var PendulumControlPanel = require( 'PENDULUM_LAB/common/view/PendulumControlPanel' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' ); var PendulumLabRulerNode = require( 'PENDULUM_LAB/common/view/PendulumLabRulerNode' ); var PeriodTraceNode = require( 'PENDULUM_LAB/common/view/PeriodTraceNode' ); var Plane = require( 'SCENERY/nodes/Plane' ); var PlaybackControlsNode = require( 'PENDULUM_LAB/common/view/PlaybackControlsNode' ); var ProtractorNode = require( 'PENDULUM_LAB/common/view/ProtractorNode' ); var ResetAllButton = require( 'SCENERY_PHET/buttons/ResetAllButton' ); var ScreenView = require( 'JOIST/ScreenView' ); var StopwatchNode = require( 'PENDULUM_LAB/common/view/StopwatchNode' ); var ToolsPanel = require( 'PENDULUM_LAB/common/view/ToolsPanel' ); var VBox = require( 'SCENERY/nodes/VBox' ); /** * @constructor * * @param {PendulumLabModel} model * @param {ModelViewTransform2} modelViewTransform */ function PendulumLabScreenView( model, options ) { ScreenView.call( this ); // @private {PendulumLabModel} this.model = model; options = _.extend( { hasGravityTweakers: false, hasPeriodTimer: false }, options ); var modelViewTransform = PendulumLabConstants.MODEL_VIEW_TRANSFORM; var pendulaNode = new PendulaNode( model.pendula, modelViewTransform, { isAccelerationVisibleProperty: model.isAccelerationVisibleProperty, isVelocityVisibleProperty: model.isVelocityVisibleProperty } ); // create drag listener for the pendula var backgroundDragNode = new Plane(); var dragListener = new ClosestDragListener( 0.15, 0 ); // 15cm from mass is OK for touch pendulaNode.draggableItems.forEach( function( draggableItem ) { dragListener.addDraggableItem( draggableItem ); } ); backgroundDragNode.addInputListener( dragListener ); // @private {PeriodTraceNode} this.firstPeriodTraceNode = new PeriodTraceNode( model.pendula[ 0 ], modelViewTransform ); this.secondPeriodTraceNode = new PeriodTraceNode( model.pendula[ 1 ], modelViewTransform ); // create protractor node var protractorNode = new ProtractorNode( model.pendula, modelViewTransform ); // create a node to keep track of combo box var popupLayer = new Node(); var pendulumControlPanel = new PendulumControlPanel( model.pendula, model.numberOfPendulaProperty ); var globalControlPanel = new GlobalControlPanel( model, popupLayer, !!options.hasGravityTweakers ); // @protected this.rightPanelsContainer = new VBox( { spacing: PendulumLabConstants.PANEL_PADDING, children: [ pendulumControlPanel, globalControlPanel ], right: this.layoutBounds.right - PendulumLabConstants.PANEL_PADDING, top: this.layoutBounds.top + PendulumLabConstants.PANEL_PADDING } ); // create tools control panel (which controls the visibility of the ruler and stopwatch) var toolsControlPanelNode = new ToolsPanel( model.ruler.isVisibleProperty, model.stopwatch.isVisibleProperty, model.isPeriodTraceVisibleProperty, options.hasPeriodTimer, { maxWidth: 180, left: this.layoutBounds.left + PendulumLabConstants.PANEL_PADDING, bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING } ); // @protected {Node} this.toolsControlPanelNode = toolsControlPanelNode; // create pendulum system control panel (controls the length and mass of the pendula) var playbackControls = new PlaybackControlsNode( model.numberOfPendulaProperty, model.isPlayingProperty, model.timeSpeedProperty, model.stepManual.bind( model ), model.returnPendula.bind( model ), { x: this.layoutBounds.centerX, bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING } ); // create reset all button var resetAllButton = new ResetAllButton( { listener: model.reset.bind( model ), right: this.layoutBounds.right - PendulumLabConstants.PANEL_PADDING, bottom: this.layoutBounds.bottom - PendulumLabConstants.PANEL_PADDING } ); // create ruler node var rulerNode = new PendulumLabRulerNode( model.ruler, modelViewTransform, this.layoutBounds ); rulerNode.left = this.layoutBounds.left + PendulumLabConstants.PANEL_PADDING; rulerNode.top = this.layoutBounds.top + PendulumLabConstants.PANEL_PADDING; model.ruler.setInitialLocationValue( rulerNode.center ); // @protected this.rulerNode = rulerNode; // create timer node var stopwatchNode = new StopwatchNode( model.stopwatch, this.layoutBounds ); stopwatchNode.bottom = rulerNode.bottom; stopwatchNode.right = Math.max( 167.75, toolsControlPanelNode.right ); // If we are only on this screen model.stopwatch.setInitialLocationValue( stopwatchNode.center ); // @protected this.stopwatchNode = stopwatchNode; // @protected this.arrowsPanelLayer = new Node(); this.energyGraphLayer = new Node(); this.periodTimerLayer = new Node(); var leftFloatingLayer = new Node( { children: [ this.energyGraphLayer, this.arrowsPanelLayer, toolsControlPanelNode ] } ); var rightFloatingLayer = new Node( { children: [ this.rightPanelsContainer, resetAllButton, popupLayer ] } ); // Layout for https://github.com/phetsims/pendulum-lab/issues/98 this.visibleBoundsProperty.lazyLink( function( visibleBounds ) { var dx = -visibleBounds.x; dx = Math.min( 200, dx ); leftFloatingLayer.x = -dx; rightFloatingLayer.x = dx; // set the drag bounds of the ruler and stopwatch rulerNode.movableDragHandler.setDragBounds( visibleBounds.erodedXY( rulerNode.width / 2, rulerNode.height / 2 ) ); stopwatchNode.movableDragHandler.setDragBounds( visibleBounds.erodedXY( stopwatchNode.width / 2, stopwatchNode.height / 2 ) ); } ); this.children = [ backgroundDragNode, protractorNode, leftFloatingLayer, rightFloatingLayer, playbackControls, this.firstPeriodTraceNode, this.secondPeriodTraceNode, pendulaNode, rulerNode, this.periodTimerLayer, stopwatchNode ]; } pendulumLab.register( 'PendulumLabScreenView', PendulumLabScreenView ); return inherit( ScreenView, PendulumLabScreenView, { /** * Steps the view. * @public * * @param {number} dt */ step: function( dt ) { if ( this.model.isPlayingProperty.value ) { this.firstPeriodTraceNode.step( dt ); this.secondPeriodTraceNode.step( dt ); } } } ); } );
define( function( require ) { 'use strict'; // modules var inherit = require( 'PHET_CORE/inherit' ); var MovableDragHandler = require( 'SCENERY_PHET/input/MovableDragHandler' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' ); var RulerNode = require( 'SCENERY_PHET/RulerNode' ); // strings var rulerUnitsString = require( 'string!PENDULUM_LAB/rulerUnits' ); // constants var RULER_HEIGHT = 34; var TICK_INTERVAL = 5; // tick interval in cm /** * @constructor * * @param {Ruler} ruler - Model for ruler. * @param {ModelViewTransform2} modelViewTransform * @param {Bounds2} layoutBounds - Bounds of screen view */ function PendulumLabRulerNode( ruler, modelViewTransform, layoutBounds ) { var self = this; // create tick labels var tickLabel; var rulerTicks = [ '' ]; // zero tick is not labeled for ( var currentTick = TICK_INTERVAL; currentTick < ruler.length * 100; currentTick += TICK_INTERVAL ) { // if the current tick is a multiple of twice the Tick interval then label it as such otherwise it is not labeled. tickLabel = currentTick % ( 2 * TICK_INTERVAL ) ? '' : currentTick.toString(); rulerTicks.push( tickLabel ); } rulerTicks.push( '' ); // last tick is not labeled // define ruler params in view coordinates var rulerWidth = modelViewTransform.modelToViewDeltaX( ruler.length ); var tickWidth = rulerWidth / ( rulerTicks.length - 1 ); RulerNode.call( this, rulerWidth, RULER_HEIGHT, tickWidth, rulerTicks, rulerUnitsString, { backgroundFill: 'rgb( 237, 225, 121 )', cursor: 'pointer', insetsWidth: 0, majorTickFont: PendulumLabConstants.RULER_FONT, majorTickHeight: 12, minorTickHeight: 6, unitsFont: PendulumLabConstants.RULER_FONT, unitsMajorTickIndex: rulerTicks.length - 3, minorTicksPerMajorTick: 4, tickMarksOnBottom: false } ); // make it a vertical ruler this.rotate( Math.PI / 2 ); // @public this.movableDragHandler = new MovableDragHandler( ruler.locationProperty, { dragBounds: layoutBounds.erodedXY( this.width / 2, this.height / 2 ) } ); // add drag and drop events this.addInputListener( this.movableDragHandler ); // add update of node location ruler.locationProperty.lazyLink( function( location ) { // because it's initially null, and will be null on a reset if ( location ) { self.center = location; } } ); // set visibility observer ruler.isVisibleProperty.linkAttribute( this, 'visible' ); } pendulumLab.register( 'PendulumLabRulerNode', PendulumLabRulerNode ); return inherit( RulerNode, PendulumLabRulerNode ); } );
define( function( require ) { 'use strict'; // modules var BooleanProperty = require( 'AXON/BooleanProperty' ); var DynamicProperty = require( 'AXON/DynamicProperty' ); var inherit = require( 'PHET_CORE/inherit' ); var Node = require( 'SCENERY/nodes/Node' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' ); var PendulumNumberControl = require( 'PENDULUM_LAB/common/view/PendulumNumberControl' ); var Property = require( 'AXON/Property' ); var Range = require( 'DOT/Range' ); var Text = require( 'SCENERY/nodes/Text' ); var Util = require( 'DOT/Util' ); // strings var frictionString = require( 'string!PENDULUM_LAB/friction' ); var lotsString = require( 'string!PENDULUM_LAB/lots' ); var noneString = require( 'string!PENDULUM_LAB/none' ); /** * Converts the numerical value of the slider to friction, does not assign to friction property * @private * * @param {number} sliderValue * @returns {number} */ function sliderValueToFriction( sliderValue ) { return 0.0005 * ( Math.pow( 2, sliderValue ) - 1 ); } /** * Converts the numerical value of the friction to a slider value, does not assign to slider property * @private * * @param {number}friction * @returns {number} */ function frictionToSliderValue( friction ) { return Util.roundSymmetric( Math.log( friction / 0.0005 + 1 ) / Math.LN2 ); } /** * @constructor * * @param {Property.<number>} frictionProperty - Property to update by slider. * @param {Range} frictionRange - Possible range of frictionProperty value. * @param {Object} [options] */ function FrictionSliderNode( frictionProperty, frictionRange, options ) { var sliderValueProperty = new DynamicProperty( new Property( frictionProperty ), { bidirectional: true, map: frictionToSliderValue, inverseMap: sliderValueToFriction } ); // range the slider can have var sliderValueRange = new Range( frictionToSliderValue( frictionRange.min ), frictionToSliderValue( frictionRange.max ) ); //TODO #210 replace '{0}' with SunConstants.VALUE_NAMED_PLACEHOLDER var numberControl = new PendulumNumberControl( frictionString, sliderValueProperty, sliderValueRange, '{0}', 'rgb(50,145,184)', { hasReadoutProperty: new BooleanProperty( false ), excludeTweakers: true, sliderPadding: 14, sliderOptions: { thumbFill: '#00C4DF', thumbFillHighlighted: '#71EDFF', minorTickLength: 5, majorTickLength: 10, constrainValue: function( value ) { return Util.roundSymmetric( value ); }, majorTicks: [ { value: sliderValueRange.min, label: new Text( noneString, { font: PendulumLabConstants.TICK_FONT, maxWidth: 50 } ) }, { value: sliderValueRange.getCenter(), label: null }, { value: sliderValueRange.max, label: new Text( lotsString, { font: PendulumLabConstants.TICK_FONT, maxWidth: 50 } ) } ], minorTickSpacing: sliderValueRange.getLength() / 10 } } ); // describes the panel box containing the friction slider Node.call( this, _.extend( { children: [ numberControl ] }, options ) ); } pendulumLab.register( 'FrictionSliderNode', FrictionSliderNode ); return inherit( Node, FrictionSliderNode ); } );
define( function( require ) { 'use strict'; // modules var BooleanProperty = require( 'AXON/BooleanProperty' ); var Emitter = require( 'AXON/Emitter' ); var inherit = require( 'PHET_CORE/inherit' ); var NumberProperty = require( 'AXON/NumberProperty' ); var pendulumLab = require( 'PENDULUM_LAB/pendulumLab' ); var PendulumLabConstants = require( 'PENDULUM_LAB/common/PendulumLabConstants' ); var PeriodTrace = require( 'PENDULUM_LAB/common/model/PeriodTrace' ); var Property = require( 'AXON/Property' ); var Range = require( 'DOT/Range' ); var Util = require( 'DOT/Util' ); var Vector2 = require( 'DOT/Vector2' ); var Vector2Property = require( 'DOT/Vector2Property' ); // constants var TWO_PI = Math.PI * 2; // scratch vector for convenience var scratchVector = new Vector2( 0, 0 ); /** * @constructor * * @param {number} index - Which pendulum in a system is this? * @param {number} mass - mass of pendulum, kg. * @param {number} length - length of pendulum, m. * @param {boolean} isVisible - Initial visibility of pendulum. * @param {Property.<number>} gravityProperty - Property with current gravity value. * @param {Property.<number>} frictionProperty - Property with current friction value. * @param {Property.<boolean>} isPeriodTraceVisibleProperty - Flag property to track checkbox value of period trace visibility. * @param {boolean} hasPeriodTimer */ function Pendulum( index, mass, length, isVisible, gravityProperty, frictionProperty, isPeriodTraceVisibleProperty, hasPeriodTimer ) { var self = this; // @public {number} this.index = index; // @public {Property.<number>} - Length of the pendulum (in meters) this.lengthProperty = new NumberProperty( length ); // @public {Property.<number>} - Mass of the pendulum (in kilograms) this.massProperty = new NumberProperty( mass ); // @public {Property.<number>} - Angle in radians (0 is straight down, positive is to the right) this.angleProperty = new NumberProperty( 0 ); // @public {Property.<number>} - Angular velocity (in radians/second) this.angularVelocityProperty = new NumberProperty( 0 ); // @public {boolean} this.hasPeriodTimer = hasPeriodTimer; /*---------------------------------------------------------------------------* * Derived variables *----------------------------------------------------------------------------*/ // @public {Property.<number>} - Angular acceleration in rad/s^2 this.angularAccelerationProperty = new NumberProperty( 0 ); // @public - Position from the rotation point this.positionProperty = new Vector2Property( Vector2.ZERO ); // @public this.velocityProperty = new Vector2Property( Vector2.ZERO ); // @public this.accelerationProperty = new Vector2Property( Vector2.ZERO ); // @public {Property.<number>} - In Joules this.kineticEnergyProperty = new NumberProperty( 0 ); // @public {Property.<number>} - In Joules this.potentialEnergyProperty = new NumberProperty( 0 ); // @public {Property.<number>} - In Joules this.thermalEnergyProperty = new NumberProperty( 0 ); // @public {Property.<boolean>} - Whether the pendulum is currently being dragged. this.isUserControlledProperty = new BooleanProperty( false ); // @public {Property.<boolean>} - Whether the pendulum tick is visible on the protractor. this.isTickVisibleProperty = new BooleanProperty( false ); // @public {Property.<boolean>} - Whether the entire pendulum is visible or not this.isVisibleProperty = new BooleanProperty( false ); // save link to global properties // @private this.gravityProperty = gravityProperty; this.frictionProperty = frictionProperty; // @public this.stepEmitter = new Emitter( { validators: [ { valueType: 'number' } ] } ); this.userMovedEmitter = new Emitter(); this.crossingEmitter = new Emitter( { validators: [ { valueType: 'number' }, { valueType: 'boolean' } ] } ); this.peakEmitter = new Emitter( { validators: [ { valueType: 'number' } ] } ); this.resetEmitter = new Emitter(); // default color for this pendulum // @public (read-only) this.color = PendulumLabConstants.PENDULUM_COLORS[ index ]; // {string} // @public {Range} (read-only) this.lengthRange = new Range( 0.1, 1.0 ); // @public {Range} (read-only) this.massRange = new Range( 0.1, 1.50 ); // @public {PeriodTrace} this.periodTrace = new PeriodTrace( this ); // If it NOT repeatable, the PeriodTimer type will control the visibility. if ( !hasPeriodTimer ) { Property.multilink( [ isPeriodTraceVisibleProperty, this.isVisibleProperty ], function( isPeriodTraceVisible, isVisible ) { self.periodTrace.isVisibleProperty.value = isPeriodTraceVisible && isVisible; } ); } // make tick on protractor visible after first drag this.isUserControlledProperty.lazyLink( function( isUserControlled ) { if ( isUserControlled ) { self.isTickVisibleProperty.value = true; // Seems like an UI-specific issue, not model self.angularVelocityProperty.value = 0; self.updateDerivedVariables( false ); // Clear thermal energy on a drag, see https://github.com/phetsims/pendulum-lab/issues/196 self.thermalEnergyProperty.value = 0; } } ); // make the angle value visible after the first drag this.angleProperty.lazyLink( function() { if ( self.isUserControlledProperty.value ) { self.updateDerivedVariables( false ); self.userMovedEmitter.emit(); } } ); // update the angular velocity when the length changes this.lengthProperty.lazyLink( function( newLength, oldLength ) { self.angularVelocityProperty.value = self.angularVelocityProperty.value * oldLength / newLength; self.updateDerivedVariables( false ); // preserve thermal energy } ); this.updateListener = this.updateDerivedVariables.bind( this, false ); // don't add thermal energy on these callbacks this.massProperty.lazyLink( this.updateListener ); gravityProperty.lazyLink( this.updateListener ); } pendulumLab.register( 'Pendulum', Pendulum ); return inherit( Object, Pendulum, { /** * Function that returns the instantaneous angular acceleration * @private * * @param {number} theta - angular position * @param {number} omega - angular velocity * @returns {number} */ omegaDerivative: function( theta, omega ) { return -this.frictionTerm( omega ) - ( this.gravityProperty.value / this.lengthProperty.value ) * Math.sin( theta ); }, /** * Function that returns the tangential drag force on the pendulum per unit mass per unit length * The friction term has units of angular acceleration. * The friction has a linear and quadratic component (with speed) * @private * * @param {number} omega - the angular velocity of the pendulum * @returns {number} */ frictionTerm: function( omega ) { return this.frictionProperty.value * this.lengthProperty.value / Math.pow( this.massProperty.value, 1 / 3 ) * omega * Math.abs( omega ) + this.frictionProperty.value / Math.pow( this.massProperty.value, 2 / 3 ) * omega; }, /** * Stepper function for the pendulum model. * It uses a Runge-Kutta approach to solve the angular differential equation * @public * * @param {number} dt */ step: function( dt ) { var theta = this.angleProperty.value; var omega = this.angularVelocityProperty.value; var numSteps = Math.max( 7, dt * 120 ); // 10 iterations typically maintains about ~11 digits of precision for total energy for ( var i = 0; i < numSteps; i++ ) { var step = dt / numSteps; // Runge Kutta (order 4), where the derivative of theta is omega. var k1 = omega * step; var l1 = this.omegaDerivative( theta, omega ) * step; var k2 = ( omega + 0.5 * l1 ) * step; var l2 = this.omegaDerivative( theta + 0.5 * k1, omega + 0.5 * l1 ) * step; var k3 = ( omega + 0.5 * l2 ) * step; var l3 = this.omegaDerivative( theta + 0.5 * k2, omega + 0.5 * l2 ) * step; var k4 = ( omega + l3 ) * step; var l4 = this.omegaDerivative( theta + k3, omega + l3 ) * step; var newTheta = Pendulum.modAngle( theta + ( k1 + 2 * k2 + 2 * k3 + k4 ) / 6 ); var newOmega = omega + ( l1 + 2 * l2 + 2 * l3 + l4 ) / 6; // did the pendulum crossed the vertical axis (from below) // is the pendulum going from left to right or vice versa, or (is the pendulum on the vertical axis and changed position ) if ( ( newTheta * theta < 0 ) || ( newTheta === 0 && theta !== 0 ) ) { this.cross( i * step, ( i + 1 ) * step, newOmega > 0, theta, newTheta ); } // did the pendulum reach a turning point // is the pendulum changing is speed from left to right or is the angular speed zero but wasn't zero on the last update if ( ( newOmega * omega < 0 ) || ( newOmega === 0 && omega !== 0 ) ) { this.peak( theta, newTheta ); } theta = newTheta; omega = newOmega; } // update the angular variables this.angleProperty.value = theta; this.angularVelocityProperty.value = omega; // update the derived variables, taking into account the transfer to thermal energy if friction is present this.updateDerivedVariables( this.frictionProperty.value > 0 ); this.stepEmitter.emit( dt ); }, /** * Function that emits when the pendulum is crossing the equilibrium point (theta=0) * Given that the time step is finite, we attempt to do a linear interpolation, to find the * precise time at which the pendulum cross the vertical. * @private * * @param {number} oldDT * @param {number} newDT * @param {boolean} isPositiveDirection * @param {number} oldTheta * @param {number} newTheta */ cross: function( oldDT, newDT, isPositiveDirection, oldTheta, newTheta ) { // If we crossed near oldTheta, our crossing DT is near oldDT. If we crossed near newTheta, our crossing DT is close // to newDT. var crossingDT = Util.linear( oldTheta, newTheta, oldDT, newDT, 0 ); this.crossingEmitter.emit( crossingDT, isPositiveDirection ); }, /** * Sends a signal that the peak angle (turning angle) has been reached * It sends the value of the peak angle * @private * * @param {number} oldTheta * @param {number} newTheta */ peak: function( oldTheta, newTheta ) { // a slightly better estimate is turningAngle = ( oldTheta + newTheta ) / 2 + (dt/2)*(oldOmega^2+newOmega^2)/(oldOmega-newOmega) var turningAngle = ( oldTheta + newTheta > 0 ) ? Math.max( oldTheta, newTheta ) : Math.min( oldTheta, newTheta ); this.peakEmitter.emit( turningAngle ); }, /** * Given the angular position and velocity, this function updates derived variables : * namely the various energies( kinetic, thermal, potential and total energy) * and the linear variables (position, velocity, acceleration) of the pendulum * @private * * @param {boolean} energyChangeToThermal - is Friction present in the model */ updateDerivedVariables: function( energyChangeToThermal ) { var speed = Math.abs( this.angularVelocityProperty.value ) * this.lengthProperty.value; this.angularAccelerationProperty.value = this.omegaDerivative( this.angleProperty.value, this.angularVelocityProperty.value ); var height = this.lengthProperty.value * ( 1 - Math.cos( this.angleProperty.value ) ); var oldKineticEnergy = this.kineticEnergyProperty.value; this.kineticEnergyProperty.value = 0.5 * this.massProperty.value * speed * speed; var oldPotentialEnergy = this.potentialEnergyProperty.value; this.potentialEnergyProperty.value = this.massProperty.value * this.gravityProperty.value * height; if ( energyChangeToThermal ) { this.thermalEnergyProperty.value += ( oldKineticEnergy + oldPotentialEnergy ) - ( this.kineticEnergyProperty.value + this.potentialEnergyProperty.value ); } this.positionProperty.value = Vector2.createPolar( this.lengthProperty.value, this.angleProperty.value - Math.PI / 2 ); this.velocityProperty.value = Vector2.createPolar( this.angularVelocityProperty.value * this.lengthProperty.value, this.angleProperty.value ); // coordinate frame -pi/2, but perpendicular +pi/2 // add up net forces for the acceleration // tangential friction this.accelerationProperty.value = Vector2.createPolar( -this.frictionTerm( this.angularVelocityProperty.value ) / this.massProperty.value, this.angleProperty.value ); // tangential gravity this.accelerationProperty.value.add( scratchVector.setPolar( -this.gravityProperty.value * Math.sin( this.angleProperty.value ), this.angleProperty.value ) ); // radial (centripetal acceleration) this.accelerationProperty.value.add( scratchVector.setPolar( this.lengthProperty.value * this.angularVelocityProperty.value * this.angularVelocityProperty.value, this.angleProperty.value + Math.PI / 2 ) ); this.velocityProperty.notifyListenersStatic(); this.accelerationProperty.notifyListenersStatic(); }, /** * Reset all the properties of this model. * @public */ reset: function() { // Note: We don't reset isVisibleProperty, since it is controlled externally. this.lengthProperty.reset(); this.massProperty.reset(); this.angleProperty.reset(); this.angularVelocityProperty.reset(); this.angularAccelerationProperty.reset(); this.positionProperty.reset(); this.velocityProperty.reset(); this.accelerationProperty.reset(); this.kineticEnergyProperty.reset(); this.potentialEnergyProperty.reset(); this.thermalEnergyProperty.reset(); this.isUserControlledProperty.reset(); this.isTickVisibleProperty.reset(); this.updateDerivedVariables( false ); }, /** * Function that determines if the pendulum is stationary, i.e. is controlled by the user or not moving * @public * * @returns {boolean} */ isStationary: function() { return this.isUserControlledProperty.value || ( this.angleProperty.value === 0 && this.angularVelocityProperty.value === 0 && this.angularAccelerationProperty.value === 0 ); }, /** * Functions returns an approximate period of the pendulum * The so-called small angle approximation is a lower bound to the true period in absence of friction * This function is currently used to fade out the path of the period trace * @public * * @returns {number} */ getApproximatePeriod: function() { return 2 * Math.PI * Math.sqrt( this.lengthProperty.value / this.gravityProperty.value ); }, /** * Resets the motion of the Pendulum * @public */ resetMotion: function() { this.angleProperty.reset(); this.angularVelocityProperty.reset(); // ticks are initially invisible this.isTickVisibleProperty.reset(); this.periodTrace.resetPathPoints(); this.updateDerivedVariables( false ); this.resetEmitter.emit(); }, /** * Resets the thermal energy to zero * @public */ resetThermalEnergy: function() { this.thermalEnergyProperty.reset(); } }, { /** * Takes our angle modulo 2pi between -pi and pi. * @public * * @param {number} angle * @returns {number} */ modAngle: function( angle ) { angle = angle % TWO_PI; if ( angle < -Math.PI ) { angle += TWO_PI; } if ( angle > Math.PI ) { angle -= TWO_PI; } return angle; } } ); } );