From a32ad0b2f177d541371b290f4e66594d7c75a611 Mon Sep 17 00:00:00 2001 From: Jonathan Olson Date: Tue, 18 Jun 2019 18:11:56 -0600 Subject: [PATCH] Moving general fraction display code to common code, see https://github.com/phetsims/fractions-intro/issues/7 --- js/MixedFractionNode.js | 211 +++++++++++++++++++++++++++++++++++++ js/PropertyFractionNode.js | 100 ++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 js/MixedFractionNode.js create mode 100644 js/PropertyFractionNode.js diff --git a/js/MixedFractionNode.js b/js/MixedFractionNode.js new file mode 100644 index 000000000..9de6346f2 --- /dev/null +++ b/js/MixedFractionNode.js @@ -0,0 +1,211 @@ +// Copyright 2019, University of Colorado Boulder + +/** + * Capable of displaying a mixed-fraction display with three spots that can be filled with numbers (numerator, + * denominator, and a whole number on the left). + * + * @author Jonathan Olson + */ +define( require => { + 'use strict'; + + // modules + const AlignBox = require( 'SCENERY/nodes/AlignBox' ); + const Bounds2 = require( 'DOT/Bounds2' ); + const sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' ); + const HBox = require( 'SCENERY/nodes/HBox' ); + const Line = require( 'SCENERY/nodes/Line' ); + const PhetFont = require( 'SCENERY_PHET/PhetFont' ); + const Text = require( 'SCENERY/nodes/Text' ); + const VBox = require( 'SCENERY/nodes/VBox' ); + + class MixedFractionNode extends HBox { + /** + * @param {Object} [options] + */ + constructor( options ) { + super( { + spacing: 5 + } ); + + options = _.extend( { + // {number|null} - Main values for the fraction (can also be changed with setters). The spot will be empty if + // null is the given value. + whole: null, + numerator: null, + denominator: null, + + // {number|null} - If provided, it will ensure that spacing is provided from 0 up to the specified number for + // that slot (e.g. if given maxNumerator:10, it will check the layout size for 0,1,2,...,10 and ensure that + // changing the numerator between those values will not change the layout). + maxWhole: null, + maxNumerator: null, + maxDenominator: null, + + // {ColorDef} + wholeFill: 'black', + numeratorFill: 'black', + denominatorFill: 'black', + separatorFill: 'black', + + // {number} - How far past the numbers' bounds that the vinculum should extend. + vinculumExtension: 0, + + // {string} - The lineCap of the vinculum + vinculumLineCap: 'butt' + }, options ); + + // @private {Text} + this.wholeText = new Text( '1', { + font: new PhetFont( 50 ), + fill: options.wholeFill + } ); + this.numeratorText = new Text( '1', { + font: new PhetFont( 30 ), + fill: options.numeratorFill + } ); + this.denominatorText = new Text( '1', { + font: new PhetFont( 30 ), + fill: options.denominatorFill + } ); + + const maxTextBounds = ( textNode, maxNumber ) => { + return _.reduce( _.range( 0, maxNumber + 1 ), ( bounds, number ) => { + textNode.text = number; + return bounds.union( textNode.bounds ); + }, Bounds2.NOTHING ); + }; + + // @private {Node} + this.wholeContainer = options.maxWhole ? new AlignBox( this.wholeText, { + alignBounds: maxTextBounds( this.wholeText, options.maxWhole ) + } ) : this.wholeText; + this.numeratorContainer = options.maxNumerator ? new AlignBox( this.numeratorText, { + alignBounds: maxTextBounds( this.numeratorText, options.maxNumerator ) + } ) : this.numeratorText; + this.denominatorContainer = options.maxDenominator ? new AlignBox( this.denominatorText, { + alignBounds: maxTextBounds( this.denominatorText, options.maxDenominator ) + } ) : this.denominatorText; + + // @private {Line} + this.vinculumNode = new Line( 0, 0, 10, 0, { + stroke: options.separatorFill, + lineWidth: 2, + lineCap: options.vinculumLineCap + } ); + + // @private {VBox} + this.vbox = new VBox( { + children: [ this.numeratorContainer, this.vinculumNode, this.denominatorContainer ], + spacing: 1 + } ); + + // @private {number|null} + this._whole = options.whole; + this._numerator = options.numerator; + this._denominator = options.denominator; + + // @private {number} + this._vinculumExtension = options.vinculumExtension; + + this.update(); + + this.mutate( options ); + } + + /** + * Updates the view of the fraction when something changes. + * @private + */ + update() { + const hasWhole = this._whole !== null; + const hasNumerator = this._numerator !== null; + const hasDenominator = this._denominator !== null; + + this.children = [ + ...( hasWhole ? [ this.wholeContainer ] : [] ), + ...( hasNumerator || hasDenominator ? [ this.vbox ] : [] ) + ]; + this.wholeText.text = hasWhole ? this._whole : ' '; + this.numeratorText.text = hasNumerator ? this._numerator : ' '; + this.denominatorText.text = hasDenominator ? this._denominator : ' '; + + this.vinculumNode.x1 = -this._vinculumExtension; + this.vinculumNode.x2 = Math.max( this.numeratorContainer.width, this.denominatorContainer.width ) + 2 + this._vinculumExtension; + } + + /** + * Sets the whole-number part of the mixed fraction. + * @public + * + * @param {number|null} value + */ + set whole( value ) { + if ( this._whole !== value ) { + this._whole = value; + + this.update(); + } + } + + /** + * Returns the current whole-number part of the mixed fraction. + * @public + * + * @returns {number|null} + */ + get whole() { + return this._whole; + } + + /** + * Sets the numerator part of the mixed fraction. + * @public + * + * @param {number|null} value + */ + set numerator( value ) { + if ( this._numerator !== value ) { + this._numerator = value; + + this.update(); + } + } + + /** + * Returns the current numerator part of the mixed fraction. + * @public + * + * @returns {number|null} + */ + get numerator() { + return this._numerator; + } + + /** + * Sets the denominator part of the mixed fraction. + * @public + * + * @param {number|null} value + */ + set denominator( value ) { + if ( this._denominator !== value ) { + this._denominator = value; + + this.update(); + } + } + + /** + * Returns the current denominator part of the mixed fraction. + * @public + * + * @returns {number|null} + */ + get denominator() { + return this._denominator; + } + } + + return sceneryPhet.register( 'MixedFractionNode', MixedFractionNode ); +} ); diff --git a/js/PropertyFractionNode.js b/js/PropertyFractionNode.js new file mode 100644 index 000000000..567a53c47 --- /dev/null +++ b/js/PropertyFractionNode.js @@ -0,0 +1,100 @@ +// Copyright 2018, University of Colorado Boulder + +/** + * Displays a fraction based on a numerator/denominator Property pair. + * + * @author Jonathan Olson + */ +define( require => { + 'use strict'; + + // modules + const Enumeration = require( 'PHET_CORE/Enumeration' ); + const sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' ); + const MixedFractionNode = require( 'SCENERY_PHET/MixedFractionNode' ); + + class PropertyFractionNode extends MixedFractionNode { + /** + * @param {Property.} numeratorProperty + * @param {Property.} denominatorProperty + * @param {Object} [options] + */ + constructor( numeratorProperty, denominatorProperty, options ) { + options = _.extend( { + // {PropertyFractionNode.DisplayType} + type: PropertyFractionNode.DisplayType.IMPROPER, + + // {boolean} + simplify: false, + + // {boolean} + showZeroImproperFraction: true + }, options ); + + assert && assert( PropertyFractionNode.DisplayType.includes( options.type ) ); + assert && assert( typeof options.simplify === 'boolean' ); + + super( options ); + + // @private {Property.} + this.numeratorProperty = numeratorProperty; + this.denominatorProperty = denominatorProperty; + + // @private {function} + this.propertyListener = this.updateFromProperties.bind( this ); + + // @private {PropertyFractionNode.DisplayType} + this.type = options.type; + + // @private {boolean} + this.simplify = options.simplify; + this.showZeroImproperFraction = options.showZeroImproperFraction; + + this.numeratorProperty.lazyLink( this.propertyListener ); + this.denominatorProperty.lazyLink( this.propertyListener ); + this.updateFromProperties(); + } + + /** + * Updates our display based on our Property values. + * @private + */ + updateFromProperties() { + const numerator = this.numeratorProperty.value; + const denominator = this.denominatorProperty.value; + + const hasWhole = this.type === PropertyFractionNode.DisplayType.IMPROPER || !this.simplify || numerator === 0 || numerator >= denominator; + const hasFraction = this.type === PropertyFractionNode.DisplayType.IMPROPER || !this.simplify || ( this.showZeroImproperFraction ? numerator > 0 : ( numerator % denominator !== 0 ) ); + + this.denominator = hasFraction ? denominator : null; + + if ( this.type === PropertyFractionNode.DisplayType.MIXED ) { + this.whole = hasWhole ? Math.floor( numerator / denominator ) : null; + this.numerator = hasFraction ? ( numerator % denominator ) : null; + } + else { + this.numerator = numerator; + } + } + + /** + * Releases references. + * @public + * @override + */ + dispose() { + this.numeratorProperty.unlink( this.propertyListener ); + this.denominatorProperty.unlink( this.propertyListener ); + + super.dispose(); + } + } + + // @public {Enumeration} + PropertyFractionNode.DisplayType = new Enumeration( [ + 'IMPROPER', // e.g. 3/2 + 'MIXED' // e.g. 1 1/2 + ] ); + + return sceneryPhet.register( 'PropertyFractionNode', PropertyFractionNode ); +} );