/** * @param {Evaporator} evaporator * @constructor */ function EvaporationControl( evaporator ) { var thisControl = this; var label = new Text( StringUtils.format( pattern_0label, evaporationString ), { font: new PhetFont( 22 ) } ); var slider = new HSlider( evaporator.evaporationRate, new Range( 0, evaporator.maxEvaporationRate ), { trackSize: new Dimension2( 200, 6 ), enabledProperty: evaporator.enabled, endDrag: function() { evaporator.evaporationRate.set( 0 ); } // at end of drag, snap evaporation rate back to zero } ); var tickFont = new PhetFont( 16 ); slider.addMajorTick( 0, new Text( noneString, { font: tickFont } ) ); slider.addMajorTick( evaporator.maxEvaporationRate, new Text( lotsString, { font: tickFont } ) ); var content = new Node(); content.addChild( label ); content.addChild( slider ); slider.left = label.right + 10; slider.centerY = label.centerY; Panel.call( thisControl, content, { xMargin: 15, yMargin: 8, fill: '#F0F0F0', stroke: 'gray', lineWidth: 1, resize: false } ); }
_.each( collection.collectionBoxes, function( collectionBox ) { var collectionBoxNode = isSingleCollectionMode ? new SingleCollectionBoxNode( collectionBox, toModelBounds ) : new MultipleCollectionBoxNode( collectionBox, toModelBounds ); selfNode.collectionBoxNodes.push( collectionBoxNode ); // TODO: can we fix this up somehow to be better? easier way to force height? // center box horizontally and put at bottom vertically in our holder function layoutBoxNode() { // compute correct offsets var offsetX = ( maximumBoxWidth - collectionBoxNode.width ) / 2; var offsetY = maximumBoxHeight - collectionBoxNode.height; // only apply these if they are different. otherwise we run into infinite recursion if ( collectionBoxNode.x !== offsetX || collectionBoxNode.y !== offsetY ) { collectionBoxNode.setTranslation( offsetX, offsetY ); } } layoutBoxNode(); // also position if its size changes in the future collectionBoxNode.addEventListener( 'bounds', layoutBoxNode ); var collectionBoxHolder = new Node(); // enforce consistent bounds of the maximum size. reason: we don't want switching between collections to alter the positions of the collection boxes collectionBoxHolder.addChild( new Rectangle( 0, 0, maximumBoxWidth, maximumBoxHeight, { visible: false, stroke: null } ) ); // TODO: Spacer node for Scenery? collectionBoxHolder.addChild( collectionBoxNode ); selfNode.addChild( collectionBoxHolder ); collectionBoxHolder.top = y; y += collectionBoxHolder.height + 15; // TODO: GeneralLayoutNode for Scenery? } );
/** * {Dimension2} size * @constructor */ function AtomicInteractionsIcon( size ) { Node.call( this ); // background var backgroundRect = new Rectangle( 0, 0, size.width, size.height, 0, 0, { fill: 'black' } ); this.addChild( backgroundRect ); // create the two atoms under a parent node var atomRadius = size.width * 0.2; var gradient = new RadialGradient( 0, 0, 0, 0, 0, atomRadius ) .addColorStop( 0, PARTICLE_COLOR ) .addColorStop( 1, PARTICLE_COLOR.darkerColor( 0.5 ) ); var atomsNode = new Node(); atomsNode.addChild( new Circle( atomRadius, { fill: gradient, opacity: 0.85, centerX: -atomRadius * 0.7 } ) ); atomsNode.addChild( new Circle( atomRadius, { fill: gradient, opacity: 0.85, centerX: atomRadius * 0.7 } ) ); // position and add the two interacting atoms atomsNode.centerX = backgroundRect.width / 2; atomsNode.centerY = backgroundRect.height / 2; this.addChild( atomsNode ); }
/** * This convenience define the "readout pointer", which is an indicator that contains a textual indication of the * average atomic mass and also has a pointer on the top that can be used to indicate the position on a linear scale. * This node is set up such that the (0,0) point is at the top center of the node, which is where the point of the * pointer exists. This is done to make it easy to position the node under the mass indication line. * * @param {MixIsotopeModel} model */ function ReadoutPointer( model ) { var node = new Node(); this.model = model; // Add the triangular pointer. This is created such that the point of the triangle is at (0,0) for this node. var vertices = [ new Vector2( -TRIANGULAR_POINTER_WIDTH / 2, TRIANGULAR_POINTER_HEIGHT ), new Vector2( TRIANGULAR_POINTER_WIDTH / 2, TRIANGULAR_POINTER_HEIGHT ), new Vector2( 0, 0 ) ]; var triangle = new Path( Shape.polygon( vertices ), { fill: new Color( 0, 143, 212 ), lineWidth: 1 } ); node.addChild( triangle ); var readoutText = new Text( '', { font: new PhetFont( 14 ), maxWidth: 0.9 * SIZE.width, maxHeight: 0.9 * SIZE.height } ); var readoutPanel = new Panel( readoutText, { minWidth: SIZE.width, minHeight: SIZE.height, resize: false, cornerRadius: 2, lineWidth: 1, align: 'center', fill: 'white' } ); readoutPanel.top = triangle.bottom; readoutPanel.centerX = triangle.centerX; node.addChild( readoutPanel ); function updateReadout( averageAtomicMass ) { var weight; if ( model.showingNaturesMixProperty.get() ) { weight = AtomIdentifier.getStandardAtomicMass( model.selectedAtomConfig.protonCount ); } else { weight = averageAtomicMass; } readoutText.setText( Util.toFixed( weight, NUMBER_DECIMALS ) + ' ' + amuString ); readoutText.centerX = SIZE.width / 2; } // Observe the average atomic weight property in the model and update the textual readout whenever it changes. // Doesn't need unlink as it stays through out the sim life model.testChamber.averageAtomicMassProperty.link( function( averageAtomicMass ) { updateReadout( averageAtomicMass ); } ); return node; }
var MAX_TITLE_WIDTH = 190; // empirically determined, supports translation /** * @param {Property<Object>} areaAndPerimeterProperty - An object containing values for area and perimeter * @param {Color} areaTextColor * @param {Color} perimeterTextColor * @param {Object} [options] * @constructor */ function AreaAndPerimeterDisplay( areaAndPerimeterProperty, areaTextColor, perimeterTextColor, options ) { options = _.extend( { maxWidth: Number.POSITIVE_INFINITY }, options ); var contentNode = new Node(); var areaCaption = new Text( areaString, { font: DISPLAY_FONT } ); var perimeterCaption = new Text( perimeterString, { font: DISPLAY_FONT } ); var tempTwoDigitString = new Text( '999', { font: DISPLAY_FONT } ); var areaReadout = new Text( '', { font: DISPLAY_FONT, fill: areaTextColor } ); var perimeterReadout = new Text( '', { font: DISPLAY_FONT, fill: perimeterTextColor } ); contentNode.addChild( areaCaption ); perimeterCaption.left = areaCaption.left; perimeterCaption.top = areaCaption.bottom + 5; contentNode.addChild( perimeterCaption ); contentNode.addChild( areaReadout ); contentNode.addChild( perimeterReadout ); var readoutsRightEdge = Math.max( perimeterCaption.right, areaCaption.right ) + 8 + tempTwoDigitString.width; areaAndPerimeterProperty.link( function( areaAndPerimeter ) { areaReadout.text = areaAndPerimeter.area; areaReadout.bottom = areaCaption.bottom; areaReadout.right = readoutsRightEdge; perimeterReadout.text = areaAndPerimeter.perimeter; perimeterReadout.bottom = perimeterCaption.bottom; perimeterReadout.right = readoutsRightEdge; } ); // in support of translation, scale the content node if it's too big if ( contentNode.width > MAX_CONTENT_WIDTH ){ contentNode.scale( MAX_CONTENT_WIDTH / contentNode.width ); } this.expandedProperty = new Property( true ); AccordionBox.call( this, contentNode, { cornerRadius: 3, titleNode: new Text( valuesString, { font: DISPLAY_FONT, maxWidth: MAX_TITLE_WIDTH } ), titleAlignX: 'left', contentAlign: 'left', fill: 'white', showTitleWhenExpanded: false, contentXMargin: 8, contentYMargin: 4, expandedProperty: this.expandedProperty, expandCollapseButtonOptions: { touchAreaXDilation: 5, touchAreaYDilation: 5 } } ); this.mutate( options ); }
var createItem = function( solute ) { var node = new Node(); var colorNode = new Rectangle( 0, 0, 20, 20, { fill: solute.maxColor, stroke: solute.maxColor.darkerColor() } ); var textNode = new Text( solute.name, { font: new PhetFont( 20 ) } ); node.addChild( colorNode ); node.addChild( textNode ); textNode.left = colorNode.right + 5; textNode.centerY = colorNode.centerY; return ComboBox.createItem( node, solute ); };
var createFace = function() { var leftEye = new Eyeball(); var rightEye = new Eyeball(); var face = new Node(); face.addChild( leftEye.mutate( { y: 300 } ) ); face.addChild( rightEye.mutate( { left: leftEye.right + leftEye.width, y: leftEye.y } ) ); // var leftEyebrow = new Eyebrow(); // var rightEyebrow = new Eyebrow(); // face.addChild( leftEyebrow.mutate( { x: leftEye.x, y: leftEye.y - 30 } ) ); // face.addChild( rightEyebrow.mutate( { x: rightEye.x, y: leftEye.y - 30, scale: new Vector2( -1, 1 ) } ) ); return face; };
function SquarePoolGrid( model, mvt ) { var self = this; Node.call( this ); var fontOptions = { font: new PhetFont( 12 ) }; var leftEdgeOfGrid = model.poolDimensions.leftChamber.centerTop - model.poolDimensions.leftChamber.widthBottom / 2; var rightEdgeOfGrid = model.poolDimensions.rightChamber.centerTop + model.poolDimensions.rightChamber.widthTop / 2; this.addChild( new GridLinesNode( model.globalModel, mvt, leftEdgeOfGrid, model.poolDimensions.leftChamber.y, rightEdgeOfGrid, model.poolDimensions.leftChamber.y + model.poolDimensions.leftChamber.height + 0.3 ) ); // Add the labels for meters var labelPosX = mvt.modelToViewX( ( model.poolDimensions.leftChamber.centerTop + model.poolDimensions.leftChamber.widthTop / 2 + model.poolDimensions.rightChamber.centerTop - model.poolDimensions.rightChamber.widthTop / 2 ) / 2 ); var slantMultiplier = 0.45; // Empirically determined to make label line up in space between the pools. var depthLabelsMeters = new Node(); for ( var depthMeters = 0; depthMeters <= model.poolDimensions.leftChamber.height; depthMeters++ ) { var metersText = new Text( depthMeters + ' ' + metersString, fontOptions ); var metersLabelRect = new Rectangle( 0, 0, metersText.width + 5, metersText.height + 5, 10, 10, {fill: '#67a257'} ); metersText.center = metersLabelRect.center; metersLabelRect.addChild( metersText ); metersLabelRect.centerX = labelPosX + mvt.modelToViewX( depthMeters * slantMultiplier ); metersLabelRect.centerY = mvt.modelToViewY( depthMeters + model.globalModel.skyGroundBoundY ); depthLabelsMeters.addChild( metersLabelRect ); } // Add the labels for feet, adjust for loop to limit number of labels. var depthLabelsFeet = new Node(); for ( var depthFeet = 0; depthFeet <= model.poolDimensions.leftChamber.height * 3.3 + 1; depthFeet += 5 ) { var feetText = new Text( depthFeet + ' ' + feetString, fontOptions ); var feetLabelRect = new Rectangle( 0, 0, feetText.width + 5, feetText.height + 5, 10, 10, {fill: '#67a257'} ); feetText.center = feetLabelRect.center; feetLabelRect.addChild( feetText ); feetLabelRect.centerX = labelPosX + mvt.modelToViewX( depthFeet / 3.3 * slantMultiplier ); feetLabelRect.centerY = mvt.modelToViewY( depthFeet / 3.3 + model.globalModel.skyGroundBoundY ); depthLabelsFeet.addChild( feetLabelRect ); } this.addChild( depthLabelsMeters ); this.addChild( depthLabelsFeet ); model.globalModel.measureUnitsProperty.link( function( value ) { var metersVisible = (value !== 'english'); depthLabelsMeters.visible = metersVisible; depthLabelsFeet.visible = !metersVisible; } ); model.globalModel.isGridVisibleProperty.link( function( value ) { self.visible = value; } ); }
sensor.activeProperty.link( active => { if ( active ) { if ( backLayer.hasChild( temperatureAndColorSensorNode ) ) { backLayer.removeChild( temperatureAndColorSensorNode ); } sensorLayer.addChild( temperatureAndColorSensorNode ); } else { if ( sensorLayer.hasChild( temperatureAndColorSensorNode ) ) { sensorLayer.removeChild( temperatureAndColorSensorNode ); } backLayer.addChild( temperatureAndColorSensorNode ); } } );
QUnit.test( 'sibling positioning', function( assert ) { const rootNode = new Node( { tagName: 'div' } ); const display = new Display( rootNode ); // eslint-disable-line document.body.appendChild( display.domElement ); // test bounds are set for basic input elements const buttonElement = new Rectangle( 5, 5, 5, 5, { tagName: 'button' } ); const divElement = new Rectangle( 0, 0, 20, 20, { tagName: 'div', focusable: true } ); const inputElement = new Rectangle( 10, 3, 25, 5, { tagName: 'input', inputType: 'range' } ); rootNode.addChild( buttonElement ); rootNode.addChild( divElement ); rootNode.addChild( inputElement ); // udpdate so the display to position elements display.updateDisplay(); assert.ok( siblingBoundsCorrect( buttonElement ), 'button element child of root correctly positioned' ); assert.ok( siblingBoundsCorrect( divElement ), 'div element child of root correctly positioned' ); assert.ok( siblingBoundsCorrect( inputElement ), 'input element child of root correctly positioned' ); // test that bounds are set correctly once we have a hierarchy and add transformations rootNode.removeChild( buttonElement ); rootNode.removeChild( divElement ); rootNode.removeChild( inputElement ); rootNode.addChild( divElement ); divElement.addChild( buttonElement ); buttonElement.addChild( inputElement ); // arbitrary transformations down the tree (should be propagated to input element) divElement.setCenter( new Vector2( 50, 50 ) ); buttonElement.setScaleMagnitude( 0.89 ); inputElement.setRotation( Math.PI / 4 ); // udpdate so the display to position elements display.updateDisplay(); assert.ok( siblingBoundsCorrect( buttonElement ), 'button element descendant correctly positioned' ); assert.ok( siblingBoundsCorrect( inputElement ), 'input element descendant correctly positioned' ); // when inner content of an element changes, its client bounds change - make sure that the element still matches // the Node buttonElement.innerHTML = 'Some Test'; display.updateDisplay(); assert.ok( siblingBoundsCorrect( buttonElement ), 'button element descendant correclty positioned after inner content changed' ); // remove the display element so it doesn't interfere with qunit api document.body.removeChild( display.domElement ); } );
function ChamberPoolGrid( model, mvt ) { var self = this; Node.call( this ); var fontOptions = { font: new PhetFont( 12 ) }; this.addChild( new GridLinesNode( model.globalModel, mvt, model.poolDimensions.leftChamber.x1, model.poolDimensions.leftOpening.y1, model.poolDimensions.rightOpening.x2, model.poolDimensions.leftChamber.y2 + 0.3 ) ); // Add the labels for meters var labelPosX = mvt.modelToViewX( ( model.poolDimensions.leftChamber.x2 + model.poolDimensions.rightOpening.x1 ) / 2 ); var depthLabelsMeters = new Node(); for ( var depthMeters = 0; depthMeters <= model.poolDimensions.leftChamber.y2 - model.globalModel.skyGroundBoundY; depthMeters++ ) { var metersText = new Text( depthMeters + ' ' + metersString, fontOptions ); var metersLabelRect = new Rectangle( 0, 0, metersText.width + 5, metersText.height + 5, 10, 10, {fill: '#67a257'} ); metersText.center = metersLabelRect.center; metersLabelRect.addChild( metersText ); metersLabelRect.centerX = labelPosX; metersLabelRect.centerY = mvt.modelToViewY( depthMeters + model.globalModel.skyGroundBoundY ); depthLabelsMeters.addChild( metersLabelRect ); } // Add the labels for feet, adjust for loop to limit number of labels. var depthLabelsFeet = new Node(); for ( var depthFeet = 0; depthFeet <= ( model.poolDimensions.leftChamber.y2 - model.globalModel.skyGroundBoundY ) * 3.3 + 1; depthFeet += 5 ) { var feetText = new Text( depthFeet + ' ' + feetString, fontOptions ); var feetLabelRect = new Rectangle( 0, 0, feetText.width + 5, feetText.height + 5, 10, 10, {fill: '#67a257'} ); feetText.center = feetLabelRect.center; feetLabelRect.addChild( feetText ); feetLabelRect.centerX = labelPosX; feetLabelRect.centerY = mvt.modelToViewY( depthFeet / 3.3 + model.globalModel.skyGroundBoundY ); depthLabelsFeet.addChild( feetLabelRect ); } this.addChild( depthLabelsMeters ); this.addChild( depthLabelsFeet ); model.globalModel.measureUnitsProperty.link( function( value ) { var metersVisible = (value !== 'english'); depthLabelsMeters.visible = metersVisible; depthLabelsFeet.visible = !metersVisible; } ); model.globalModel.isGridVisibleProperty.link( function( value ) { self.visible = value; } ); }
QUnit.test( 'change', assert => { const rootNode = new Node( { tagName: 'div' } ); const display = new Display( rootNode ); // eslint-disable-line beforeTest( display ); const a = new Rectangle( 0, 0, 20, 20, { tagName: 'input', inputType: 'range' } ); let gotFocus = false; let gotChange = false; rootNode.addChild( a ); a.addInputListener( { focus() { gotFocus = true; }, change() { gotChange = true; }, blur() { gotFocus = false; } } ); a.accessibleInstances[ 0 ].peer.primarySibling.focus(); assert.ok( gotFocus && !gotChange, 'focus first' ); dispatchEvent( a.accessibleInstances[ 0 ].peer.primarySibling, 'change' ); assert.ok( gotChange && gotFocus, 'a should have been an input' ); afterTest( display ); } );
function Arrow( model, x, y, rotation ) { Node.call( this, { x: x, y: y, rotation: (rotation / 180 * Math.PI) } ); var arrow = new Node(); var arrowShape = new Shape(); var points = [ [ 5, -30 ], [ 13, -30 ], [ 13, 13 ], [ -25, 13 ], [ -25, 17 ], [ -40, 8.5 ], [ -25, 0 ], [ -25, 5 ], [ 5, 5 ] ]; arrowShape.moveTo( points[ 0 ][ 0 ], points[ 0 ][ 1 ] ); _.each( points, function( element ) { arrowShape.lineTo( element[ 0 ], element[ 1 ] ); } ); arrowShape.close(); arrow.addChild( new Path( arrowShape, { stroke: "#000", fill: "#F00", lineWidth: 0.2 } ) ); this.addChild( arrow ); model.currentProperty.link( function( current ) { // Scale the arrows based on the value of the current. // Exponential scaling algorithm. Linear makes the changes too big. var scale = Math.pow( ( current * 0.1 ), 0.7 ); arrow.matrix = Matrix3.scale( scale ); } ); }
var createItem = function( dataSet ) { var node = new Node(); // label var textNode = new Text( dataSet.name, { font: LSRConstants.TEXT_FONT } ); node.addChild( textNode ); return ComboBox.createItem( node, dataSet ); };
model.savedQuadraticProperty.link( savedQuadratic => { // remove and dispose any previously-saved line if ( savedLineNode ) { allLinesParent.removeChild( savedLineNode ); savedLineNode.dispose(); savedLineNode = null; } if ( savedQuadratic ) { savedLineNode = new QuadraticNode( new Property( savedQuadratic ), model.graph.xRange, model.graph.yRange, model.modelViewTransform, viewProperties.equationForm, viewProperties.equationsVisibleProperty, { lineWidth: GQConstants.SAVED_QUADRATIC_LINE_WIDTH, preventVertexAndEquationOverlap: options.preventVertexAndEquationOverlap } ); // Add it in the foreground, so the user can see it. // See https://github.com/phetsims/graphing-quadratics/issues/36 allLinesParent.addChild( savedLineNode ); } } );
/** * * @param {DnaMolecule} dnaMolecule * @param {ModelViewTransform2} modelViewTransform * @param {number} backboneStrokeWidth * @param {boolean} showGeneBracketLabels * @constructor */ function DnaMoleculeNode( dnaMolecule, modelViewTransform, backboneStrokeWidth, showGeneBracketLabels ) { Node.call( this ); // Add the layers onto which the various nodes that represent parts of the dna, the hints, etc. are placed. var geneBackgroundLayer = new Node(); this.addChild( geneBackgroundLayer ); // Layers for supporting the 3D look by allowing the "twist" to be depicted. this.dnaBackboneLayer = new DnaMoleculeCanvasNode( dnaMolecule, modelViewTransform, backboneStrokeWidth, { canvasBounds: new Bounds2( dnaMolecule.getLeftEdgeXPosition(), dnaMolecule.getBottomEdgeYPosition() + modelViewTransform.viewToModelDeltaY( 10 ), dnaMolecule.getRightEdgeXPosition(), dnaMolecule.getTopEdgeYPosition() - modelViewTransform.viewToModelDeltaY( 10 ) ), matrix: modelViewTransform.getMatrix() } ); this.addChild( this.dnaBackboneLayer ); // Put the gene backgrounds and labels behind everything. for ( var i = 0; i < dnaMolecule.getGenes().length; i++ ) { geneBackgroundLayer.addChild( new GeneNode( modelViewTransform, dnaMolecule.getGenes()[ i ], dnaMolecule, StringUtils.fillIn( geneString, { geneID: i + 1 } ), showGeneBracketLabels ) ); } }
/** * @param {UnderPressureModel} underPressureModel of the sim * @param {ModelViewTransform2 } modelViewTransform to convert between model and view co-ordinates * @param {number} poolLeftX is pool left x coordinate * @param {number} poolTopY is pool top y coordinate * @param {number} poolRightX is pool right x coordinate * @param {number} poolBottomY is pool bottom y coordinate * @param {number} poolHeight is height of the pool * @param {number} labelXPosition is label x position * @param {number} slantMultiplier is to make label line up in space between the pools * @constructor */ function TrapezoidPoolGrid( underPressureModel, modelViewTransform, poolLeftX, poolTopY, poolRightX, poolBottomY, poolHeight, labelXPosition, slantMultiplier ) { Node.call( this ); var fontOptions = { font: new PhetFont( 12 ), maxWidth: 20 }; // add grid line this.addChild( new GridLinesNode( underPressureModel.measureUnitsProperty, modelViewTransform, poolLeftX, poolTopY, poolRightX, poolBottomY ) ); // Add the labels for meters var depthLabelsMeters = new Node(); for ( var depthMeters = 0; depthMeters <= poolHeight; depthMeters++ ) { var metersText = new Text( StringUtils.format( valueWithUnitsPatternString, depthMeters, mString ), fontOptions ); var metersLabelRect = new Rectangle( 0, 0, metersText.width + 5, metersText.height + 5, 10, 10, { fill: '#67a257' } ); metersText.center = metersLabelRect.center; metersLabelRect.addChild( metersText ); metersLabelRect.centerX = labelXPosition + modelViewTransform.modelToViewX( depthMeters * slantMultiplier ); metersLabelRect.centerY = modelViewTransform.modelToViewY( -depthMeters ); depthLabelsMeters.addChild( metersLabelRect ); } // Add the labels for feet, adjust for loop to limit number of labels. var depthLabelsFeet = new Node(); for ( var depthFeet = 0; depthFeet <= poolHeight * 3.3 + 1; depthFeet += 5 ) { var feetText = new Text( StringUtils.format( valueWithUnitsPatternString, depthFeet, ftString ), fontOptions ); var feetLabelRect = new Rectangle( 0, 0, feetText.width + 5, feetText.height + 5, 10, 10, { fill: '#67a257' } ); feetText.center = feetLabelRect.center; feetLabelRect.addChild( feetText ); feetLabelRect.centerX = labelXPosition + modelViewTransform.modelToViewDeltaX( depthFeet / 3.3 * slantMultiplier ); feetLabelRect.centerY = modelViewTransform.modelToViewY( -depthFeet / 3.3 ); depthLabelsFeet.addChild( feetLabelRect ); } this.addChild( depthLabelsMeters ); this.addChild( depthLabelsFeet ); underPressureModel.measureUnitsProperty.link( function( measureUnits ) { depthLabelsFeet.visible = (measureUnits === 'english'); depthLabelsMeters.visible = (measureUnits !== 'english'); } ); underPressureModel.isGridVisibleProperty.linkAttribute( this, 'visible' ); }
QUnit.test( 'focusin/focusout (focus/blur)', assert => { const rootNode = new Node( { tagName: 'div' } ); const display = new Display( rootNode ); // eslint-disable-line beforeTest( display ); const a = new Rectangle( 0, 0, 20, 20, { tagName: 'button' } ); let aGotFocus = false; let aLostFocus = false; let bGotFocus = false; rootNode.addChild( a ); a.addInputListener( { focus() { aGotFocus = true; }, blur() { aLostFocus = true; } } ); a.focus(); assert.ok( aGotFocus, 'a should have been focused' ); assert.ok( !aLostFocus, 'a should not blur' ); const b = new Rectangle( 0, 0, 20, 20, { tagName: 'button' } ); // TODO: what if b was child of a, make sure these events don't bubble! rootNode.addChild( b ); b.addInputListener( { focus() { bGotFocus = true; } } ); b.focus(); assert.ok( bGotFocus, 'b should have been focused' ); assert.ok( aLostFocus, 'a should have lost focused' ); afterTest( display ); } );
/** * @constructor * @param {BASEModel} model * @param {BalloonModel} balloonModel * @param {BalloonNode} balloonNode * @param {Bounds2} layoutBounds */ function BalloonInteractionCueNode( model, balloonModel, balloonNode, layoutBounds ) { Node.call( this ); // create the help node for the WASD and arrow keys, invisible except for on the initial balloon pick up var directionKeysParent = new Node(); this.addChild( directionKeysParent ); var wNode = this.createMovementKeyNode( 'up' ); var aNode = this.createMovementKeyNode( 'left' ); var sNode = this.createMovementKeyNode( 'down' ); var dNode = this.createMovementKeyNode( 'right' ); directionKeysParent.addChild( wNode ); directionKeysParent.addChild( aNode ); directionKeysParent.addChild( sNode ); directionKeysParent.addChild( dNode ); // add listeners to update visibility of nodes when location changes and when the wall is made // visible/invisible Property.multilink( [ balloonModel.locationProperty, model.wall.isVisibleProperty ], function( location, visible ) { // get the max x locations depending on if the wall is visible var centerXRightBoundary; if ( visible ) { centerXRightBoundary = PlayAreaMap.X_LOCATIONS.AT_WALL; } else { centerXRightBoundary = PlayAreaMap.X_BOUNDARY_LOCATIONS.AT_RIGHT_EDGE; } var balloonCenter = balloonModel.getCenter(); aNode.visible = balloonCenter.x !== PlayAreaMap.X_BOUNDARY_LOCATIONS.AT_LEFT_EDGE; sNode.visible = balloonCenter.y !== PlayAreaMap.Y_BOUNDARY_LOCATIONS.AT_BOTTOM; dNode.visible = balloonCenter.x !== centerXRightBoundary; wNode.visible = balloonCenter.y !== PlayAreaMap.Y_BOUNDARY_LOCATIONS.AT_TOP; } ); // place the direction cues relative to the balloon bounds var balloonBounds = balloonModel.bounds; wNode.centerBottom = balloonBounds.getCenterTop().plusXY( 0, -BALLOON_KEY_SPACING ); aNode.rightCenter = balloonBounds.getLeftCenter().plusXY( -BALLOON_KEY_SPACING, 0 ); sNode.centerTop = balloonBounds.getCenterBottom().plusXY( 0, BALLOON_KEY_SPACING + SHADOW_WIDTH ); dNode.leftCenter = balloonBounds.getRightCenter().plusXY( BALLOON_KEY_SPACING + SHADOW_WIDTH, 0 ); }
var createItem = function( solute ) { var node = new Node(); // color chip var soluteColor = solute.stockColor; var colorNode = new Rectangle( 0, 0, 20, 20, { fill: soluteColor, stroke: soluteColor.darkerColor() } ); // label var textNode = new Text( StringUtils.format( pattern_0name_1pH, solute.name, Util.toFixed( solute.pH, PHScaleConstants.PH_COMBO_BOX_DECIMAL_PLACES ) ), { font: new PhetFont( 22 ) } ); node.addChild( colorNode ); node.addChild( textNode ); textNode.left = colorNode.right + 5; textNode.centerY = colorNode.centerY; return ComboBox.createItem( node, solute ); };
QUnit.test( 'Using a color instance', function( assert ) { var scene = new Node(); var rect = new Rectangle( 0, 0, 100, 50 ); assert.ok( rect.fill === null, 'Always starts with a null fill' ); scene.addChild( rect ); var color = new Color( 255, 0, 0 ); rect.fill = color; color.setRGBA( 0, 255, 0, 1 ); } );
function createTestNodeTree() { // eslint-disable-line no-unused-vars var node = new Node(); node.addChild( new Node() ); node.addChild( new Node() ); node.addChild( new Node() ); node.children[ 0 ].addChild( new Node() ); node.children[ 0 ].addChild( new Node() ); node.children[ 0 ].addChild( new Node() ); node.children[ 0 ].addChild( new Node() ); node.children[ 0 ].addChild( new Node() ); node.children[ 0 ].children[ 1 ].addChild( new Node() ); node.children[ 0 ].children[ 3 ].addChild( new Node() ); node.children[ 0 ].children[ 3 ].addChild( new Node() ); node.children[ 0 ].children[ 3 ].children[ 0 ].addChild( new Node() ); return node; }
/** * @param {Property.<number>} scaleProperty - Scale property for updating. * @param {Range} range - Working range of slider. * @param {number} step step of scale changes * @param {boolean} isIncrease flag for defining type of button * @constructor */ function SliderButton( scaleProperty, range, step, isIncrease ) { // create default view var sample = new Node( { children: [ new Rectangle( 0, 0, BUTTON_SIZE, BUTTON_SIZE, 2, 2, { fill: '#DBD485' } ), new Rectangle( 4, BUTTON_SIZE / 2 - 1, BUTTON_SIZE - 8, 2, { fill: 'black' } ) ] } ); // increase or decrease view if ( isIncrease ) { sample.addChild( new Rectangle( BUTTON_SIZE / 2 - 1, 4, 2, BUTTON_SIZE - 8, { fill: 'black' } ) ); } RectangularPushButton.call( this, { content: sample, xMargin: 0, yMargin: 0, listener: function() { scaleProperty.value = Math.max( Math.min( scaleProperty.value + (isIncrease ? step : -step), range.max ), range.min ); } } ); var self = this; // add disabling effect for buttons if ( isIncrease ) { // plus button scaleProperty.link( function( scaleValue ) { self.enabled = (scaleValue !== range.max); } ); } else { // minus button scaleProperty.link( function( scaleValue ) { self.enabled = (scaleValue !== range.min); } ); } // Increase the touch area in all directions except toward the slider knob, // so that they won't interfere too much on touch devices var dilationSize = 15; var dilateTop = ( isIncrease ) ? dilationSize : 0; var dilateBottom = ( isIncrease ) ? 0 : dilationSize; this.touchArea = Shape.bounds( new Bounds2( this.localBounds.minX - dilationSize, this.localBounds.minY - dilateTop, this.localBounds.maxX + dilationSize, this.localBounds.maxY + dilateBottom ) ); }
/** * Convenience function for creating tick marks. This includes both the actual mark and the label. * @param {NumberAtom} isotopeConfig */ function IsotopeTickMark( isotopeConfig ) { var node = new Node(); // Create the tick mark itself. It is positioned such that (0,0) is the center of the mark. var shape = new Line( 0, -TICK_MARK_LINE_HEIGHT / 2, 0, TICK_MARK_LINE_HEIGHT / 2, { lineWidth: TICK_MARK_LINE_WIDTH, stroke: 'black' } ); node.addChild( shape ); // Create the label that goes above the tick mark. var label = new RichText( ' <sup>' + isotopeConfig.massNumberProperty.get() + '</sup>' + AtomIdentifier.getSymbol( isotopeConfig.protonCountProperty.get() ), { font: new PhetFont( 12 ) } ); label.centerX = shape.centerX; label.bottom = shape.top; node.addChild( label ); return node; }
QUnit.test( 'setting accessible order on nodes with no accessible content', function( assert ) { var rootNode = new Node(); var display = new Display( rootNode ); // eslint-disable-line document.body.appendChild( display.domElement ); var a = new Node( { tagName: 'div' } ); var b = new Node(); var c = new Node( { tagName: 'div' } ); var d = new Node( { tagName: 'div' } ); var e = new Node( { tagName: 'div' } ); var f = new Node( { tagName: 'div' } ); rootNode.addChild( a ); a.addChild( b ); b.addChild( c ); b.addChild( e ); c.addChild( d ); c.addChild( f ); a.accessibleOrder = [ e, c ]; var divA = a.accessibleInstances[ 0 ].peer.primarySibling; var divC = c.accessibleInstances[ 0 ].peer.primarySibling; var divE = e.accessibleInstances[ 0 ].peer.primarySibling; assert.ok( divA.children[ 0 ] === divE, 'div E should be first child of div B' ); assert.ok( divA.children[ 1 ] === divC, 'div C should be second child of div B' ); } );
QUnit.test( 'Bounds and Visible Bounds', function( assert ) { var node = new Node(); var rect = new Rectangle( 0, 0, 100, 50 ); node.addChild( rect ); assert.ok( node.visibleBounds.equals( new Bounds2( 0, 0, 100, 50 ) ), 'Visible Bounds Visible' ); assert.ok( node.bounds.equals( new Bounds2( 0, 0, 100, 50 ) ), 'Complete Bounds Visible' ); rect.visible = false; assert.ok( node.visibleBounds.equals( Bounds2.NOTHING ), 'Visible Bounds Invisible' ); assert.ok( node.bounds.equals( new Bounds2( 0, 0, 100, 50 ) ), 'Complete Bounds Invisible' ); } );
createSlopeToolIcon: function( width ) { var parentNode = new Node(); // slope tool var slopeToolNode = new SlopeToolNode( new Property( Line.createSlopeIntercept( 1, 2, 0 ) ), ModelViewTransform2.createOffsetXYScaleMapping( Vector2.ZERO, 26, -26 ) ); parentNode.addChild( slopeToolNode ); // dashed line where the line would be, tweaked visually var lineNode = new Path( Shape.lineSegment( slopeToolNode.left + ( 0.4 * slopeToolNode.width ), slopeToolNode.bottom, slopeToolNode.right, slopeToolNode.top + ( 0.5 * slopeToolNode.height ) ), { lineWidth: 1, lineDash: [ 6, 6 ], stroke: 'black' } ); parentNode.addChild( lineNode ); parentNode.scale( width / parentNode.width ); return parentNode; },
/** * Creates and return a node containing the radio buttons that allow the user to select the display mode for the scale. * * @param {Property} displayModeProperty */ function DisplayModeSelectionNode( displayModeProperty ) { var radioButtonRadius = 6; var LABEL_FONT = new PhetFont( 14 ); var massNumberButton = new AquaRadioButton( displayModeProperty, DISPLAY_MODE.MASS_NUMBER, new Text( massNumberString, { font: LABEL_FONT, maxWidth: 125, fill: 'white' } ), { radius: radioButtonRadius } ); var atomicMassButton = new AquaRadioButton( displayModeProperty, DISPLAY_MODE.ATOMIC_MASS, new Text( atomicMassString, { font: LABEL_FONT, maxWidth: 125, fill: 'white' } ), { radius: radioButtonRadius } ); var displayButtonGroup = new Node(); displayButtonGroup.addChild( massNumberButton ); atomicMassButton.top = massNumberButton.bottom + 8; atomicMassButton.left = displayButtonGroup.left; displayButtonGroup.addChild( atomicMassButton ); return displayButtonGroup; }
QUnit.test( 'click extra', assert => { // create a node const a1 = new Node( { tagName: 'button' } ); const root = new Node( { tagName: 'div' } ); const display = new Display( root ); // eslint-disable-line beforeTest( display ); root.addChild( a1 ); assert.ok( a1.inputListeners.length === 0, 'no input accessible listeners on instantiation' ); assert.ok( a1.labelContent === null, 'no label on instantiation' ); // add a listener const listener = { click: function() { a1.labelContent = TEST_LABEL; } }; a1.addInputListener( listener ); assert.ok( a1.inputListeners.length === 1, 'accessible listener added' ); // verify added with hasInputListener assert.ok( a1.hasInputListener( listener ) === true, 'found with hasInputListener' ); // fire the event a1.accessibleInstances[ 0 ].peer.primarySibling.click(); assert.ok( a1.labelContent === TEST_LABEL, 'click fired, label set' ); // remove the listener a1.removeInputListener( listener ); assert.ok( a1.inputListeners.length === 0, 'accessible listener removed' ); // verify removed with hasInputListener assert.ok( a1.hasInputListener( listener ) === false, 'not found with hasInputListener' ); // make sure event listener was also removed from DOM element // click should not change the label a1.labelContent = TEST_LABEL_2; assert.ok( a1.labelContent === TEST_LABEL_2, 'before click' ); // setting the label redrew the pdom, so get a reference to the new dom element. a1.accessibleInstances[ 0 ].peer.primarySibling.click(); assert.ok( a1.labelContent === TEST_LABEL_2, 'click should not change label' ); // verify disposal removes accessible input listeners a1.addInputListener( listener ); a1.dispose(); // TODO: Since converting to use Node.inputListeners, we can't assume this anymore // assert.ok( a1.hasInputListener( listener ) === false, 'disposal removed accessible input listeners' ); afterTest( display ); } );
QUnit.test( 'Global KeyStateTracker tests', assert => { const rootNode = new Node( { tagName: 'div' } ); const display = new Display( rootNode ); // eslint-disable-line beforeTest( display ); const a = new Node( { tagName:'button' } ); const b = new Node( { tagName:'button' } ); const c = new Node( { tagName:'button' } ); const d = new Node( { tagName:'button' } ); a.addChild( b ); b.addChild( c ); c.addChild( d ); rootNode.addChild( a ); const dPrimarySibling = d.accessibleInstances[ 0 ].peer.primarySibling; triggerDOMEvent( 'keydown', dPrimarySibling, KeyboardUtil.KEY_RIGHT_ARROW ); assert.ok( Display.keyStateTracker.isKeyDown( KeyboardUtil.KEY_RIGHT_ARROW ), 'global keyStateTracker should be updated with right arrow key down' ); afterTest( display ); } );