From 8999552de6edabdd1fbb8be532bf3c1df53564d3 Mon Sep 17 00:00:00 2001 From: zepumph Date: Thu, 21 Oct 2021 16:32:50 -0600 Subject: [PATCH] finished first pass on common view (still PDOM properties and some other common code are broken), https://github.com/phetsims/ratio-and-proportion/issues/404 --- js/common/model/RAPModel.ts | 4 +- js/common/model/RAPRatioTuple.ts | 4 +- js/common/model/RatioTerm.ts | 17 ++----- js/common/view/CueDisplay.ts | 9 +++- js/common/view/RAPScreenView.ts | 3 +- js/common/view/RAPTickMarkLabelsNode.ts | 18 +++++-- js/common/view/RatioHalf.ts | 47 ++++++++++--------- js/common/view/RatioHalfTickMarksNode.ts | 13 +++-- js/common/view/RatioHandNode.ts | 28 +++++++---- .../view/TickMarkViewRadioButtonGroup.ts | 13 ++--- 10 files changed, 91 insertions(+), 65 deletions(-) diff --git a/js/common/model/RAPModel.ts b/js/common/model/RAPModel.ts index 17b032ba..78d39352 100644 --- a/js/common/model/RAPModel.ts +++ b/js/common/model/RAPModel.ts @@ -18,7 +18,7 @@ import NumberIO from '../../../../tandem/js/types/NumberIO.js'; import ratioAndProportion from '../../ratioAndProportion.js'; import rapConstants from '../rapConstants.js'; import RAPRatio from './RAPRatio.js'; -import RatioTerm, { RatioTermType } from './RatioTerm.js'; +import RatioTerm from './RatioTerm.js'; import Tandem from '../../../../tandem/js/Tandem.js'; import RAPRatioTuple from './RAPRatioTuple.js'; @@ -257,7 +257,7 @@ unclampedFitness: ${unclampedFitness} * @returns {number} * @public */ - getIdealValueForTerm( ratioTerm: RatioTermType ) { + getIdealValueForTerm( ratioTerm: RatioTerm ) { if ( ratioTerm === RatioTerm.ANTECEDENT ) { return this.targetRatioProperty.value * this.ratio.tupleProperty.value.consequent; } diff --git a/js/common/model/RAPRatioTuple.ts b/js/common/model/RAPRatioTuple.ts index 7b9aca96..b939ec58 100644 --- a/js/common/model/RAPRatioTuple.ts +++ b/js/common/model/RAPRatioTuple.ts @@ -10,7 +10,7 @@ import IOType from '../../../../tandem/js/types/IOType.js'; import NumberIO from '../../../../tandem/js/types/NumberIO.js'; import Range from '../../../../dot/js/Range.js'; import ratioAndProportion from '../../ratioAndProportion.js'; -import RatioTerm, { RatioTermType } from './RatioTerm.js'; +import RatioTerm from './RatioTerm.js'; class RAPRatioTuple { @@ -115,7 +115,7 @@ class RAPRatioTuple { * @param {RatioTerm} ratioTerm * @returns {number} */ - getForTerm( ratioTerm: RatioTermType ) { + getForTerm( ratioTerm: RatioTerm ) { switch( ratioTerm ) { case RatioTerm.ANTECEDENT: return this.antecedent; diff --git a/js/common/model/RatioTerm.ts b/js/common/model/RatioTerm.ts index 9508326b..5ed95683 100644 --- a/js/common/model/RatioTerm.ts +++ b/js/common/model/RatioTerm.ts @@ -6,19 +6,10 @@ * @author Michael Kauzmann (PhET Interactive Simulations) */ -import Enumeration from '../../../../phet-core/js/Enumeration.js'; -import ratioAndProportion from '../../ratioAndProportion.js'; - -type RatioTermType = { - ANTECEDENT: Object; - CONSEQUENT: Object; -} - // @ts-ignore -const RatioTerm = Enumeration.byKeys( [ 'ANTECEDENT', 'CONSEQUENT' ] ); - - -ratioAndProportion.register( 'RatioTerm', RatioTerm ); +enum RatioTerm { + ANTECEDENT, + CONSEQUENT +} -export { RatioTermType }; export default RatioTerm; diff --git a/js/common/view/CueDisplay.ts b/js/common/view/CueDisplay.ts index 28b39a70..f9d6d0e8 100644 --- a/js/common/view/CueDisplay.ts +++ b/js/common/view/CueDisplay.ts @@ -6,5 +6,12 @@ * @author Michael Kauzmann (PhET Interactive Simulations) */ -type CueDisplay = 'NONE'| 'W_S'| 'UP_DOWN'| 'ARROWS'; + +enum CueDisplay { + NONE, + W_S, + UP_DOWN, + ARROWS +} + export default CueDisplay; diff --git a/js/common/view/RAPScreenView.ts b/js/common/view/RAPScreenView.ts index 2202984b..5ca18dd7 100644 --- a/js/common/view/RAPScreenView.ts +++ b/js/common/view/RAPScreenView.ts @@ -51,6 +51,7 @@ import TickMarkView, { TickMarkViewType } from './TickMarkView.js'; import TickMarkViewRadioButtonGroup from './TickMarkViewRadioButtonGroup.js'; import RAPModel from '../model/RAPModel.js'; import Tandem from '../../../../tandem/js/Tandem.js'; +import CueDisplay from './CueDisplay.js'; // constants const LAYOUT_BOUNDS = ScreenView.DEFAULT_LAYOUT_BOUNDS; @@ -181,7 +182,7 @@ class RAPScreenView extends ScreenView { handColorProperty: options.leftHandColorProperty, accessibleName: ratioAndProportionStrings.a11y.leftHand, a11yDependencies: a11yDependencies, - bothHandsCueDisplay: 'W_S', + bothHandsCueDisplay: CueDisplay.W_S, isRight: false, // this way we get a left hand // Added to the antecedent for ease, but it applies to both RatioHalfs in the PDOM diff --git a/js/common/view/RAPTickMarkLabelsNode.ts b/js/common/view/RAPTickMarkLabelsNode.ts index 85588d3a..280dd2d1 100644 --- a/js/common/view/RAPTickMarkLabelsNode.ts +++ b/js/common/view/RAPTickMarkLabelsNode.ts @@ -11,12 +11,19 @@ import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; import Node from '../../../../scenery/js/nodes/Node.js'; import Text from '../../../../scenery/js/nodes/Text.js'; import ratioAndProportion from '../../ratioAndProportion.js'; -import TickMarkView from './TickMarkView.js'; +import TickMarkView, { TickMarkViewType } from './TickMarkView.js'; +import Color from '../../../../scenery/js/util/Color.js'; const LABEL_X = 0; class RAPTickMarkLabelsNode extends Node { + private totalHeight: number; + private heightOfText: number | null; + private tickMarkViewProperty: Property; + private tickMarkRangeProperty: Property; + private colorProperty: Property; + /** * @param {Property.} tickMarkViewProperty * @param {Property.} tickMarkRangeProperty @@ -24,7 +31,8 @@ class RAPTickMarkLabelsNode extends Node { * @param {Property.} colorProperty * @param {Object} [options] */ - constructor( tickMarkViewProperty, tickMarkRangeProperty, height, colorProperty, options ) { + constructor( tickMarkViewProperty: Property, tickMarkRangeProperty: Property, height: number, + colorProperty: Property, options?: any ) { if ( options ) { assert && assert( !options.hasOwnProperty( 'children' ), 'RAPTickMarkLabelsNode sets its own children' ); @@ -62,7 +70,7 @@ class RAPTickMarkLabelsNode extends Node { /** * @public */ - layout( height ) { + layout( height:number ) { this.totalHeight = height; this.update( this.tickMarkRangeProperty.value, this.tickMarkViewProperty.value ); @@ -71,7 +79,7 @@ class RAPTickMarkLabelsNode extends Node { /** * @private */ - update( tickMarkRange, tickMarkView ) { + update( tickMarkRange: number, tickMarkView: TickMarkViewType ) { // subtract one to account for potential rounding errors. This helps guarantee that the last line is drawn. const horizontalSpacing = ( this.totalHeight - 1 ) / tickMarkRange; @@ -86,7 +94,7 @@ class RAPTickMarkLabelsNode extends Node { * @param {boolean} showTickMarkUnits * @param {number} horizontalSpacing */ - updateUnitLabels( showTickMarkUnits, horizontalSpacing ) { + updateUnitLabels( showTickMarkUnits: boolean, horizontalSpacing: number ) { this.children = []; assert && assert( typeof horizontalSpacing === 'number', 'Unit Labels only supported for horizontal lines' ); diff --git a/js/common/view/RatioHalf.ts b/js/common/view/RatioHalf.ts index 731f83e1..3f142a37 100644 --- a/js/common/view/RatioHalf.ts +++ b/js/common/view/RatioHalf.ts @@ -26,7 +26,7 @@ import Rectangle from '../../../../scenery/js/nodes/Rectangle.js'; import Tandem from '../../../../tandem/js/Tandem.js'; import ratioAndProportion from '../../ratioAndProportion.js'; import ratioAndProportionStrings from '../../ratioAndProportionStrings.js'; -import RatioTerm, { RatioTermType } from '../model/RatioTerm.js'; +import RatioTerm from '../model/RatioTerm.js'; import rapConstants from '../rapConstants.js'; import CueDisplay from './CueDisplay.js'; import RatioHalfTickMarksNode from './RatioHalfTickMarksNode.js'; @@ -48,12 +48,12 @@ const SNAP_TO_TICK_MARK_THRESHOLD = 0.1; // total horizontal drag distance; const X_MODEL_DRAG_DISTANCE = 1; const INITIAL_X_VALUE = 0; -const getModelBoundsFromRange = range => new Bounds2( -1 * X_MODEL_DRAG_DISTANCE / 2, range.min, X_MODEL_DRAG_DISTANCE / 2, range.max ); +const getModelBoundsFromRange = ( range: Range ) => new Bounds2( -1 * X_MODEL_DRAG_DISTANCE / 2, range.min, X_MODEL_DRAG_DISTANCE / 2, range.max ); const MIN_HAND_SCALE = 1.2; const MAX_HAND_SCALE = 2.5; -function ratioHalfAccessibleNameBehavior( node, options, accessibleName, callbacksForOtherNodes ) { +function ratioHalfAccessibleNameBehavior( node: RatioHalf, options: any, accessibleName: string, callbacksForOtherNodes: { (): void }[] ) { callbacksForOtherNodes.push( () => { node.ratioHandNode.accessibleName = accessibleName; } ); @@ -62,17 +62,22 @@ function ratioHalfAccessibleNameBehavior( node, options, accessibleName, callbac const TOTAL_RANGE = rapConstants.TOTAL_RATIO_TERM_VALUE_RANGE; +type LayoutFunction = ( bounds: Bounds2, heightScalar: number ) => void; + class RatioHalf extends Rectangle { - public readonly framingRectangleHeight: number; + public framingRectangleHeight: number; public readonly isBeingInteractedWithProperty: BooleanProperty; private ratioLockedProperty: BooleanProperty; private bothHandsDescriber: BothHandsDescriber; private handPositionsDescriber: HandPositionsDescriber; private tickMarkViewProperty: Property; - private ratioTerm: RatioTermType; + private ratioTerm: RatioTerm; private ratioTupleProperty: Property; + ratioHandNode: RatioHandNode; + private layoutRatioHalf: LayoutFunction; + private resetRatioHalf: () => void; /** * @param {Object} config @@ -151,7 +156,7 @@ class RatioHalf extends Rectangle { a11yDependencies: [], // {CueDisplay} - bothHandsCueDisplay: 'UP_DOWN', + bothHandsCueDisplay: CueDisplay.UP_DOWN, // phet-io tandem: Tandem.REQUIRED, @@ -197,9 +202,9 @@ class RatioHalf extends Rectangle { bothHandsInteractedWith: boolean, displayBothHands: boolean ) => { return displayBothHands ? config.bothHandsCueDisplay : - keyboardFocused && !interactedWithKeyboard ? 'UP_DOWN' : - ( interactedWithKeyboard || interactedWithMouse || bothHandsInteractedWith ) ? 'NONE' : - 'ARROWS'; + keyboardFocused && !interactedWithKeyboard ? CueDisplay.UP_DOWN : + ( interactedWithKeyboard || interactedWithMouse || bothHandsInteractedWith ) ? CueDisplay.NONE : + CueDisplay.ARROWS; } ); @@ -209,10 +214,10 @@ class RatioHalf extends Rectangle { bidirectional: true, reentrant: true, valueType: 'number', - map: ratioTuple => ratioTuple.getForTerm( this.ratioTerm ), - inverseMap: term => this.ratioTerm === RatioTerm.ANTECEDENT ? this.ratioTupleProperty.value.withAntecedent( term ) : - this.ratioTerm === RatioTerm.CONSEQUENT ? this.ratioTupleProperty.value.withConsequent( term ) : - assert && assert( false, `unexpected ratioTerm ${this.ratioTerm}` ) + map: ( ratioTuple: RAPRatioTuple ) => ratioTuple.getForTerm( this.ratioTerm ), + inverseMap: ( term: number ) => this.ratioTerm === RatioTerm.ANTECEDENT ? this.ratioTupleProperty.value.withAntecedent( term ) : + this.ratioTerm === RatioTerm.CONSEQUENT ? this.ratioTupleProperty.value.withConsequent( term ) : + assert && assert( false, `unexpected ratioTerm ${this.ratioTerm}` ) } ); // @private - The draggable element inside the Node framed with thick rectangles on the top and bottom. @@ -244,7 +249,7 @@ class RatioHalf extends Rectangle { config.bounds ); // Snap mouse/touch input to the nearest tick mark if close enough. This helps with reproducible precision - const getSnapToTickMarkValue = yValue => { + const getSnapToTickMarkValue = ( yValue: number ) => { if ( TickMarkView.displayHorizontal( config.tickMarkViewProperty.value ) ) { const tickMarkStep = 1 / config.tickMarkRangeProperty.value; @@ -269,8 +274,8 @@ class RatioHalf extends Rectangle { reentrant: true, bidirectional: true, valueType: Vector2, - inverseMap: vector2 => vector2.y, - map: number => { + inverseMap: ( vector2: Vector2 ) => vector2.y, + map: ( number: number ) => { // initial case if ( mappingInitialValue ) { @@ -286,7 +291,7 @@ class RatioHalf extends Rectangle { const dragBoundsProperty = new Property( new Bounds2( 0, 0, 1, 1 ) ); // When set to a value, the horizontal position will not be changed throughout the whole drag. Set to null when not dragging. - let startingX = null; + let startingX: null | number = null; // transform and dragBounds set in layout code below const dragListener = new DragListener( { @@ -342,7 +347,7 @@ class RatioHalf extends Rectangle { } ); // When the range changes, update the dragBounds of the drag listener - config.enabledRatioTermsRangeProperty.link( enabledRange => { + config.enabledRatioTermsRangeProperty.link( ( enabledRange: Range ) => { const newBounds = getModelBoundsFromRange( enabledRange ); // offset the bounds to account for the ratioHandNode's size, since the center of the ratioHandNode is controlled by the drag bounds. @@ -379,7 +384,7 @@ class RatioHalf extends Rectangle { config.bounds.width, config.bounds.height - 2 * this.framingRectangleHeight, config.colorProperty ); - const updatePointer = position => { + const updatePointer = ( position: Vector2 ) => { this.ratioHandNode.translation = modelViewTransform.modelToViewPosition( position ); }; positionProperty.link( updatePointer ); @@ -466,7 +471,7 @@ class RatioHalf extends Rectangle { * @public * @param {number} desiredBottom */ - setBottomOfRatioHalf( desiredBottom ) { + setBottomOfRatioHalf( desiredBottom: number ) { // `selfBounds` is used for the position of the Rectangle, since RatioHalf extends Rectangle this.bottom = desiredBottom + ( this.bounds.bottom - this.localToParentBounds( this.selfBounds ).bottom ); @@ -477,7 +482,7 @@ class RatioHalf extends Rectangle { * @param {Bounds2} bounds - the bounds of this RatioHalf, effects dimensions, dragBounds, and width of guiding rectangles * @param {number} heightScalar - normalized between 0 and 1. When 1, it the ratio half will be the tallest it gets, at 0, the shortest */ - layout( bounds, heightScalar ) { + layout( bounds: Bounds2, heightScalar: number ) { assert && assert( heightScalar >= 0 && heightScalar <= 1, 'scalar should be between 0 and 1' ); this.layoutRatioHalf( bounds, heightScalar ); } diff --git a/js/common/view/RatioHalfTickMarksNode.ts b/js/common/view/RatioHalfTickMarksNode.ts index 48806cd2..bf0ed99a 100644 --- a/js/common/view/RatioHalfTickMarksNode.ts +++ b/js/common/view/RatioHalfTickMarksNode.ts @@ -11,10 +11,14 @@ import Property from '../../../../axon/js/Property.js'; import GridNode from '../../../../griddle/js/GridNode.js'; import merge from '../../../../phet-core/js/merge.js'; import ratioAndProportion from '../../ratioAndProportion.js'; -import TickMarkView from './TickMarkView.js'; +import TickMarkView, { TickMarkViewType } from './TickMarkView.js'; +import Color from '../../../../scenery/js/util/Color'; class RatioHalfTickMarksNode extends GridNode { + private tickMarkViewProperty: Property; + private tickMarkRangeProperty: Property; + /** * @param {Property.} tickMarkViewProperty * @param {Property.} tickMarkRangeProperty @@ -23,7 +27,8 @@ class RatioHalfTickMarksNode extends GridNode { * @param {Property.} colorProperty * @param {Object} [options] */ - constructor( tickMarkViewProperty, tickMarkRangeProperty, width, height, colorProperty, options ) { + constructor( tickMarkViewProperty: Property, tickMarkRangeProperty: Property, width: number, + height: number, colorProperty: Property, options?: object ) { options = merge( { // initial line spacings @@ -46,7 +51,7 @@ class RatioHalfTickMarksNode extends GridNode { /** * @public */ - layout( width, height ) { + layout( width: number, height: number ) { this.setGridWidth( width ); this.setGridHeight( height ); this.update( this.tickMarkRangeProperty.value, this.tickMarkViewProperty.value ); @@ -55,7 +60,7 @@ class RatioHalfTickMarksNode extends GridNode { /** * @private */ - update( tickMarkRange, tickMarkView ) { + update( tickMarkRange: number, tickMarkView: TickMarkViewType ) { // subtract one to account for potential rounding errors. This helps guarantee that the last line is drawn. this.setLineSpacings( { diff --git a/js/common/view/RatioHandNode.ts b/js/common/view/RatioHandNode.ts index 1d90523f..e4001301 100644 --- a/js/common/view/RatioHandNode.ts +++ b/js/common/view/RatioHandNode.ts @@ -26,10 +26,15 @@ import rapConstants from '../rapConstants.js'; import CueDisplay from './CueDisplay.js'; import getKeyboardInputSnappingMapper from './getKeyboardInputSnappingMapper.js'; import RAPColors from './RAPColors.js'; -import TickMarkView from './TickMarkView.js'; +import TickMarkView, { TickMarkViewType } from './TickMarkView.js'; +import EnumerationProperty from '../../../../axon/js/EnumerationProperty.js'; +import Color from '../../../../scenery/js/util/Color.js'; class RatioHandNode extends Node { + private focusHighlight: FocusHighlightFromNode; + private resetRatioHandNode: () => void; + /** * @param {Property.} valueProperty * @param {Property.} enabledRatioTermsRangeProperty @@ -41,8 +46,11 @@ class RatioHandNode extends Node { * @param {Property.} inProportionProperty - if the model is in proportion * @param {Object} [options] */ - constructor( valueProperty, enabledRatioTermsRangeProperty, tickMarkViewProperty, keyboardStep, colorProperty, - cueDisplayProperty, getIdealValue, inProportionProperty, options ) { + constructor( valueProperty: Property, enabledRatioTermsRangeProperty: Property, + tickMarkViewProperty: EnumerationProperty, keyboardStep: number, + colorProperty: Property, + cueDisplayProperty: Property, getIdealValue: ( n?: number ) => number, + inProportionProperty: Property, options?: any ) { const shiftKeyboardStep = rapConstants.toFixed( keyboardStep * rapConstants.SHIFT_KEY_MULTIPLIER ); // eslint-disable-line bad-sim-text @@ -62,7 +70,7 @@ class RatioHandNode extends Node { // Because this interaction uses the keyboard, snap to the keyboard step to handle the case where the hands were // previously moved via mouse/touch. See https://github.com/phetsims/ratio-and-proportion/issues/156 - a11yMapValue: ( newValue, oldValue ) => { + a11yMapValue: ( newValue: number, oldValue: number ) => { return mapKeyboardInput( newValue, oldValue, this.shiftKeyDown, inProportionProperty.value ); } }, options ); @@ -93,7 +101,7 @@ class RatioHandNode extends Node { this.focusHighlight = new FocusHighlightFromNode( handContainer ); // Only display the "cut-out target circles" when the tick marks are being shown - tickMarkViewProperty.link( tickMarkView => { + tickMarkViewProperty.link( ( tickMarkView: TickMarkViewType ) => { const displayCutOut = TickMarkView.displayHorizontal( tickMarkView ); cutOutHandNode.visible = displayCutOut; filledInHandNode.visible = !displayCutOut; @@ -141,7 +149,7 @@ class RatioHandNode extends Node { this.addChild( upCue ); this.addChild( downCue ); - cueDisplayProperty.link( cueDisplay => { + cueDisplayProperty.link( ( cueDisplay: CueDisplay ) => { cueArrowUp.visible = cueArrowDown.visible = cueDisplay === CueDisplay.ARROWS; cueArrowKeyUp.visible = cueArrowKeyDown.visible = cueDisplay === CueDisplay.UP_DOWN; cueWKeyUp.visible = cueSKeyDown.visible = cueDisplay === CueDisplay.W_S; @@ -185,7 +193,7 @@ class RatioHandNode extends Node { * @returns {Node} * @public */ - static createIcon( isRight, tickMarkViewProperty, options ) { + static createIcon( isRight: boolean, tickMarkViewProperty: EnumerationProperty, options?: any ) { options = merge( { handColor: 'black', handNodeOptions: { @@ -201,7 +209,7 @@ class RatioHandNode extends Node { new Property( options.handColor ), new Property( CueDisplay.NONE ), _.identity, - _.stubFalse, + new Property( false ), merge( { isRight: isRight, asIcon: true, @@ -219,7 +227,7 @@ class FilledInHandPath extends Path { /** * @param {Object} [options] */ - constructor( options ) { + constructor( options?: any ) { options = merge( { stroke: 'black', @@ -245,7 +253,7 @@ class CutOutHandPath extends Path { /** * @param {Object} [options] */ - constructor( options ) { + constructor( options?: any ) { options = merge( { stroke: 'black', diff --git a/js/common/view/TickMarkViewRadioButtonGroup.ts b/js/common/view/TickMarkViewRadioButtonGroup.ts index 9125170a..538e7124 100644 --- a/js/common/view/TickMarkViewRadioButtonGroup.ts +++ b/js/common/view/TickMarkViewRadioButtonGroup.ts @@ -14,7 +14,8 @@ import RectangularRadioButtonGroup from '../../../../sun/js/buttons/RectangularR import ActivationUtterance from '../../../../utterance-queue/js/ActivationUtterance.js'; import ratioAndProportion from '../../ratioAndProportion.js'; import ratioAndProportionStrings from '../../ratioAndProportionStrings.js'; -import TickMarkView from './TickMarkView.js'; +import TickMarkView, { TickMarkViewType } from './TickMarkView.js'; +import Property from '../../../../axon/js/Property.js'; // constants const ICON_SCALE = 0.45; @@ -22,10 +23,10 @@ const ICON_SCALE = 0.45; class TickMarkViewRadioButtonGroup extends RectangularRadioButtonGroup { /** - * @param {Property}tickMarkViewProperty + * @param {Property.} tickMarkViewProperty * @param {Object} [options] */ - constructor( tickMarkViewProperty, options ) { + constructor( tickMarkViewProperty: Property, options?: any ) { options = merge( { orientation: 'horizontal', @@ -57,7 +58,7 @@ class TickMarkViewRadioButtonGroup extends RectangularRadioButtonGroup { options ); const tickMarkContextResponseUtterance = new ActivationUtterance(); - tickMarkViewProperty.lazyLink( tickMarkView => { + tickMarkViewProperty.lazyLink( ( tickMarkView: TickMarkViewType ) => { switch( tickMarkView ) { case TickMarkView.NONE: @@ -84,7 +85,7 @@ class NumberedTickMarksIconPath extends Path { /** * @param {Object} [options] */ - constructor( options ) { + constructor( options?: any ) { options = merge( { fill: 'black', @@ -118,7 +119,7 @@ class TickMarksIconPath extends Path { /** * @param {Object} [options] */ - constructor( options ) { + constructor( options?: any ) { options = merge( { fill: 'black',