reset: function() { // reset the left and right pipe position and scale this.leftPipeNode.setMatrix( Matrix3.translation( this.layoutBounds.minX - this.leftPipeXOffest, this.groundY + this.pipeNodeYOffset ) ); this.leftPipeNode.scale( this.options.pipeScale, this.options.pipeScale, false ); this.leftPipeBackNode.setMatrix( Matrix3.translation( this.layoutBounds.minX - this.leftPipeXOffest, this.groundY + this.pipeNodeYOffset ) ); this.leftPipeBackNode.scale( this.options.pipeScale, this.options.pipeScale, false ); this.rightPipeNode.setMatrix( Matrix3.translation( this.layoutBounds.maxX - this.rightPipeXOffest, this.groundY + this.pipeNodeYOffset ) ); this.rightPipeNode.scale( this.options.pipeScale, this.options.pipeScale, false ); this.leftPipeMainHandleNode.setTranslation( this.layoutBounds.minX - 10, this.leftPipeNode.getCenterY() ); this.rightPipeMainHandleNode.setTranslation( this.layoutBounds.maxX - 50, this.rightPipeNode.getCenterY() ); // mark pipe as dirty for getting new spline cross sections this.pipe.dirty = true; this.pipe.createSpline(); this.updatePipeFlowLineShape(); this.flowModel.fluxMeter.trigger( 'update' ); // reset the distance from left/right pipe main drag handle to left/right pipe top/bottom control points this.yDiffFromLeftPipeDrageHandleToLeftTopControlPoint = 1.05; //model value this.yDiffFromLeftPipeDrageHandleToLeftBottomControlPoint = 1.05; this.yDiffFromRightPipeDrageHandleToRightTopControlPoint = 1.05; this.yDiffFromRightPipeDrageHandleToRightBottomControlPoint = 1.05; // reset the handle positions var numControlPoints = this.pipe.controlPoints.length; this.controlHandleNodes[ numControlPoints / 2 - 1 ].bottom = this.rightPipeNode.top + 2; this.controlHandleNodes[ numControlPoints / 2 ].top = this.rightPipeNode.bottom - 2; this.controlHandleNodes[ 0 ].bottom = this.leftPipeNode.top + 2; this.controlHandleNodes[ numControlPoints - 1 ].top = this.leftPipeNode.bottom - 2; this.gridInjectorNode.setTranslation( this.modelViewTransform.modelToViewX( this.gridInjectorX ) - this.gridInjectorNodeXOffset, this.modelViewTransform.modelToViewY( this.pipe.getCrossSection( this.gridInjectorX ).yTop ) - this.gridInjectorNodeYOffset ); }
this.positionProperty.link( position => { // shape used when determining if a given chunk of light energy should be absorbed. It is created at (0,0) relative // to the solar panel, so its position needs to be adjusted when the solar panel changes its position. It cannot // just use a relative position to the solar panel because energy chunks that are positioned globally need to check // to see if they are located within this shape, so it needs a global position as well. The untranslated version of // this shape is needed to draw the helper shape node in SolarPanelNode. // @private {Shape} this.absorptionShape = this.untranslatedAbsorptionShape.transformed( Matrix3.translation( position.x, position.y ) ); } );
// attempts to map the bounds specified to the entire testing canvas (minus a fine border), so we can nail down the location quickly function idealTransform( bounds ) { // so that the bounds-edge doesn't land squarely on the boundary var borderSize = 2; var scaleX = ( resolution - borderSize * 2 ) / ( bounds.maxX - bounds.minX ); var scaleY = ( resolution - borderSize * 2 ) / ( bounds.maxY - bounds.minY ); var translationX = -scaleX * bounds.minX + borderSize; var translationY = -scaleY * bounds.minY + borderSize; return new Transform3( Matrix3.translation( translationX, translationY ).timesMatrix( Matrix3.scaling( scaleX, scaleY ) ) ); }
QUnit.test( 'Inverse / Multiplication tests', function( assert ) { approximateMatrixEqual( assert, Matrix3.IDENTITY.inverted(), Matrix3.IDENTITY, 'I * I = I' ); approximateMatrixEqual( assert, Matrix3.IDENTITY.timesMatrix( A() ), A(), 'I * A = A' ); approximateMatrixEqual( assert, A().timesMatrix( Matrix3.IDENTITY ), A(), 'A * I = A' ); var translation = Matrix3.translation( 2, -5 ); var rotation = Matrix3.rotation2( Math.PI / 6 ); var scale = Matrix3.scale( 2, 3 ); approximateMatrixEqual( assert, translation.timesMatrix( translation.inverted() ), Matrix3.IDENTITY, 'translation inverse check' ); approximateMatrixEqual( assert, rotation.timesMatrix( rotation.inverted() ), Matrix3.IDENTITY, 'rotation inverse check' ); approximateMatrixEqual( assert, scale.timesMatrix( scale.inverted() ), Matrix3.IDENTITY, 'scale inverse check' ); approximateMatrixEqual( assert, A().timesMatrix( A().inverted() ), Matrix3.IDENTITY, 'A inverse check' ); approximateMatrixEqual( assert, B().timesMatrix( B().inverted() ), Matrix3.IDENTITY, 'B inverse check' ); approximateMatrixEqual( assert, C().timesMatrix( C().inverted() ), Matrix3.IDENTITY, 'C inverse check' ); } );
QUnit.test( 'Matrix scaling()', function( assert ) { var rotation = Matrix3.rotation2( Math.PI / 4 ); var translation = Matrix3.translation( 20, 30 ); var scale2 = Matrix3.scaling( 2 ); var scale2x3y = Matrix3.scaling( 2, 3 ); // the basics, just to make sure it is working assert.equal( scale2.getScaleVector().x, 2, 'normal x scale' ); assert.equal( scale2.getScaleVector().y, 2, 'normal y scale' ); assert.equal( scale2x3y.getScaleVector().x, 2, 'normal x scale' ); assert.equal( scale2x3y.getScaleVector().y, 3, 'normal y scale' ); var combination = rotation.timesMatrix( scale2 ).timesMatrix( translation ); approximateEquals( assert, combination.getScaleVector().x, 2, 'rotated x scale' ); approximateEquals( assert, combination.getScaleVector().y, 2, 'rotated x scale' ); } );
ListenerTestUtils.simpleRectangleTest( function( display, rect, node ) { var locationProperty = new Vector2Property( Vector2.ZERO ); var transform = new Transform3( Matrix3.translation( 5, 3 ).timesMatrix( Matrix3.scale( 2 ) ).timesMatrix( Matrix3.rotation2( Math.PI / 4 ) ) ); // Starts at 5,3 locationProperty.link( function( location ) { rect.translation = transform.transformPosition2( location ); } ); var listener = new DragListener( { locationProperty: locationProperty, transform: transform } ); rect.addInputListener( listener ); ListenerTestUtils.mouseMove( display, 10, 10 ); ListenerTestUtils.mouseDown( display, 10, 10 ); ListenerTestUtils.mouseMove( display, 20, 15 ); ListenerTestUtils.mouseUp( display, 20, 15 ); assert.equal( rect.x, 15, '[x] Started at 5, moved by 10' ); assert.equal( rect.y, 8, '[y] Started at 3, moved by 5' ); } );
canvasAccurateBounds: function( renderToContext, options ) { // how close to the actual bounds do we need to be? var precision = ( options && options.precision ) ? options.precision : 0.001; // 512x512 default square resolution var resolution = ( options && options.resolution ) ? options.resolution : 128; // at 1/16x default, we want to be able to get the bounds accurately for something as large as 16x our initial resolution // divisible by 2 so hopefully we avoid more quirks from Canvas rendering engines var initialScale = ( options && options.initialScale ) ? options.initialScale : ( 1 / 16 ); var minBounds = Bounds2.NOTHING; var maxBounds = Bounds2.EVERYTHING; var canvas = document.createElement( 'canvas' ); canvas.width = resolution; canvas.height = resolution; var context = canvas.getContext( '2d' ); if ( debugChromeBoundsScanning ) { $( window ).ready( function() { var header = document.createElement( 'h2' ); $( header ).text( 'Bounds Scan' ); $( '#display' ).append( header ); } ); } // TODO: Don't use Transform3 unless it is necessary function scan( transform ) { // save/restore, in case the render tries to do any funny stuff like clipping, etc. context.save(); transform.matrix.canvasSetTransform( context ); renderToContext( context ); context.restore(); var data = context.getImageData( 0, 0, resolution, resolution ); var minMaxBounds = Util.scanBounds( data, resolution, transform ); function snapshotToCanvas( snapshot ) { var canvas = document.createElement( 'canvas' ); canvas.width = resolution; canvas.height = resolution; var context = canvas.getContext( '2d' ); context.putImageData( snapshot, 0, 0 ); $( canvas ).css( 'border', '1px solid black' ); $( window ).ready( function() { //$( '#display' ).append( $( document.createElement( 'div' ) ).text( 'Bounds: ' + ) ); $( '#display' ).append( canvas ); } ); } // TODO: remove after debug if ( debugChromeBoundsScanning ) { snapshotToCanvas( data ); } context.clearRect( 0, 0, resolution, resolution ); return minMaxBounds; } // attempts to map the bounds specified to the entire testing canvas (minus a fine border), so we can nail down the location quickly function idealTransform( bounds ) { // so that the bounds-edge doesn't land squarely on the boundary var borderSize = 2; var scaleX = ( resolution - borderSize * 2 ) / ( bounds.maxX - bounds.minX ); var scaleY = ( resolution - borderSize * 2 ) / ( bounds.maxY - bounds.minY ); var translationX = -scaleX * bounds.minX + borderSize; var translationY = -scaleY * bounds.minY + borderSize; return new Transform3( Matrix3.translation( translationX, translationY ).timesMatrix( Matrix3.scaling( scaleX, scaleY ) ) ); } var initialTransform = new Transform3(); // make sure to initially center our object, so we don't miss the bounds initialTransform.append( Matrix3.translation( resolution / 2, resolution / 2 ) ); initialTransform.append( Matrix3.scaling( initialScale ) ); var coarseBounds = scan( initialTransform ); minBounds = minBounds.union( coarseBounds.minBounds ); maxBounds = maxBounds.intersection( coarseBounds.maxBounds ); var tempMin; var tempMax; var refinedBounds; // minX tempMin = maxBounds.minY; tempMax = maxBounds.maxY; while ( isFinite( minBounds.minX ) && isFinite( maxBounds.minX ) && Math.abs( minBounds.minX - maxBounds.minX ) > precision ) { // use maximum bounds except for the x direction, so we don't miss things that we are looking for refinedBounds = scan( idealTransform( new Bounds2( maxBounds.minX, tempMin, minBounds.minX, tempMax ) ) ); if ( minBounds.minX <= refinedBounds.minBounds.minX && maxBounds.minX >= refinedBounds.maxBounds.minX ) { // sanity check - break out of an infinite loop! if ( debugChromeBoundsScanning ) { console.log( 'warning, exiting infinite loop!' ); console.log( 'transformed "min" minX: ' + idealTransform( new Bounds2( maxBounds.minX, maxBounds.minY, minBounds.minX, maxBounds.maxY ) ).transformPosition2( p( minBounds.minX, 0 ) ) ); console.log( 'transformed "max" minX: ' + idealTransform( new Bounds2( maxBounds.minX, maxBounds.minY, minBounds.minX, maxBounds.maxY ) ).transformPosition2( p( maxBounds.minX, 0 ) ) ); } break; } minBounds = minBounds.withMinX( Math.min( minBounds.minX, refinedBounds.minBounds.minX ) ); maxBounds = maxBounds.withMinX( Math.max( maxBounds.minX, refinedBounds.maxBounds.minX ) ); tempMin = Math.max( tempMin, refinedBounds.maxBounds.minY ); tempMax = Math.min( tempMax, refinedBounds.maxBounds.maxY ); } // maxX tempMin = maxBounds.minY; tempMax = maxBounds.maxY; while ( isFinite( minBounds.maxX ) && isFinite( maxBounds.maxX ) && Math.abs( minBounds.maxX - maxBounds.maxX ) > precision ) { // use maximum bounds except for the x direction, so we don't miss things that we are looking for refinedBounds = scan( idealTransform( new Bounds2( minBounds.maxX, tempMin, maxBounds.maxX, tempMax ) ) ); if ( minBounds.maxX >= refinedBounds.minBounds.maxX && maxBounds.maxX <= refinedBounds.maxBounds.maxX ) { // sanity check - break out of an infinite loop! if ( debugChromeBoundsScanning ) { console.log( 'warning, exiting infinite loop!' ); } break; } minBounds = minBounds.withMaxX( Math.max( minBounds.maxX, refinedBounds.minBounds.maxX ) ); maxBounds = maxBounds.withMaxX( Math.min( maxBounds.maxX, refinedBounds.maxBounds.maxX ) ); tempMin = Math.max( tempMin, refinedBounds.maxBounds.minY ); tempMax = Math.min( tempMax, refinedBounds.maxBounds.maxY ); } // minY tempMin = maxBounds.minX; tempMax = maxBounds.maxX; while ( isFinite( minBounds.minY ) && isFinite( maxBounds.minY ) && Math.abs( minBounds.minY - maxBounds.minY ) > precision ) { // use maximum bounds except for the y direction, so we don't miss things that we are looking for refinedBounds = scan( idealTransform( new Bounds2( tempMin, maxBounds.minY, tempMax, minBounds.minY ) ) ); if ( minBounds.minY <= refinedBounds.minBounds.minY && maxBounds.minY >= refinedBounds.maxBounds.minY ) { // sanity check - break out of an infinite loop! if ( debugChromeBoundsScanning ) { console.log( 'warning, exiting infinite loop!' ); } break; } minBounds = minBounds.withMinY( Math.min( minBounds.minY, refinedBounds.minBounds.minY ) ); maxBounds = maxBounds.withMinY( Math.max( maxBounds.minY, refinedBounds.maxBounds.minY ) ); tempMin = Math.max( tempMin, refinedBounds.maxBounds.minX ); tempMax = Math.min( tempMax, refinedBounds.maxBounds.maxX ); } // maxY tempMin = maxBounds.minX; tempMax = maxBounds.maxX; while ( isFinite( minBounds.maxY ) && isFinite( maxBounds.maxY ) && Math.abs( minBounds.maxY - maxBounds.maxY ) > precision ) { // use maximum bounds except for the y direction, so we don't miss things that we are looking for refinedBounds = scan( idealTransform( new Bounds2( tempMin, minBounds.maxY, tempMax, maxBounds.maxY ) ) ); if ( minBounds.maxY >= refinedBounds.minBounds.maxY && maxBounds.maxY <= refinedBounds.maxBounds.maxY ) { // sanity check - break out of an infinite loop! if ( debugChromeBoundsScanning ) { console.log( 'warning, exiting infinite loop!' ); } break; } minBounds = minBounds.withMaxY( Math.max( minBounds.maxY, refinedBounds.minBounds.maxY ) ); maxBounds = maxBounds.withMaxY( Math.min( maxBounds.maxY, refinedBounds.maxBounds.maxY ) ); tempMin = Math.max( tempMin, refinedBounds.maxBounds.minX ); tempMax = Math.min( tempMax, refinedBounds.maxBounds.maxX ); } if ( debugChromeBoundsScanning ) { console.log( 'minBounds: ' + minBounds ); console.log( 'maxBounds: ' + maxBounds ); } var result = new Bounds2( // Do finite checks so we don't return NaN ( isFinite( minBounds.minX ) && isFinite( maxBounds.minX ) ) ? ( minBounds.minX + maxBounds.minX ) / 2 : Number.POSITIVE_INFINITY, ( isFinite( minBounds.minY ) && isFinite( maxBounds.minY ) ) ? ( minBounds.minY + maxBounds.minY ) / 2 : Number.POSITIVE_INFINITY, ( isFinite( minBounds.maxX ) && isFinite( maxBounds.maxX ) ) ? ( minBounds.maxX + maxBounds.maxX ) / 2 : Number.NEGATIVE_INFINITY, ( isFinite( minBounds.maxY ) && isFinite( maxBounds.maxY ) ) ? ( minBounds.maxY + maxBounds.maxY ) / 2 : Number.NEGATIVE_INFINITY ); // extra data about our bounds result.minBounds = minBounds; result.maxBounds = maxBounds; result.isConsistent = maxBounds.containsBounds( minBounds ); result.precision = Math.max( Math.abs( minBounds.minX - maxBounds.minX ), Math.abs( minBounds.minY - maxBounds.minY ), Math.abs( minBounds.maxX - maxBounds.maxX ), Math.abs( minBounds.maxY - maxBounds.maxY ) ); // return the average return result; },
define( require => { 'use strict'; // modules const BooleanProperty = require( 'AXON/BooleanProperty' ); const circuitConstructionKitCommon = require( 'CIRCUIT_CONSTRUCTION_KIT_COMMON/circuitConstructionKitCommon' ); const ConventionalCurrentArrowNode = require( 'CIRCUIT_CONSTRUCTION_KIT_COMMON/view/ConventionalCurrentArrowNode' ); const ElectronChargeNode = require( 'SCENERY_PHET/ElectronChargeNode' ); const Image = require( 'SCENERY/nodes/Image' ); const Matrix3 = require( 'DOT/Matrix3' ); const Tandem = require( 'TANDEM/Tandem' ); const Util = require( 'DOT/Util' ); // constants const ELECTRON_CHARGE_NODE = new ElectronChargeNode( { // Electrons are transparent to convey they are a representation rather than physical objects // Workaround for https://github.com/phetsims/circuit-construction-kit-dc/issues/160 sphereOpacity: 0.75, minusSignOpacity: 0.75, // selected so an electron will exactly fit the width of a wire scale: 0.78 } ).toDataURLImageSynchronous(); const ARROW_NODE = new ConventionalCurrentArrowNode( Tandem.rootTandem.createTandem( 'arrowNode' ) ) .toDataURLImageSynchronous(); const ARROW_OFFSET = Matrix3.translation( -ARROW_NODE.width / 2, -ARROW_NODE.height / 2 ); const HALF_ROTATION = Matrix3.rotation2( Math.PI ); // scratch matrix that is used to set values to scenery const NODE_MATRIX = new Matrix3(); // Below this amperage, no conventional current will be rendered. const CONVENTIONAL_CHARGE_THRESHOLD = 1E-6; // position the electron--note the offsets that were used to make it look exactly centered, see // https://github.com/phetsims/circuit-construction-kit-dc/issues/104 const ELECTRON_OFFSET = Matrix3.translation( -ELECTRON_CHARGE_NODE.width / 2 - 0.5, -ELECTRON_CHARGE_NODE.height / 2 - 0.5 ); class ChargeNode extends Image { /** * @param {Charge} charge - the model element */ constructor( charge ) { const child = charge.charge > 0 ? ARROW_NODE : ELECTRON_CHARGE_NODE; super( child.image, { pickable: false } ); // @public (read-only) {Charge} - the model depicted by this node this.charge = charge; this.outsideOfBlackBoxProperty = new BooleanProperty( false ); // Update the visibility accordingly. A multilink will not work because the charge circuitElement changes. this.updateVisibleListener = this.updateVisible.bind( this ); // When the model position changes, update the node position this.updateTransformListener = this.updateTransform.bind( this ); charge.changedEmitter.addListener( this.updateTransformListener ); charge.visibleProperty.link( this.updateVisibleListener ); this.outsideOfBlackBoxProperty.link( this.updateVisibleListener ); charge.disposeEmitterCharge.addListener( this.dispose.bind( this ) ); this.updateTransformListener(); } /** * Dispose resources when no longer used. * @public */ dispose() { this.charge.changedEmitter.removeListener( this.updateTransformListener ); this.charge.visibleProperty.unlink( this.updateVisibleListener ); this.outsideOfBlackBoxProperty.unlink( this.updateVisibleListener ); super.dispose(); } /** * @private - update the transform of the charge node */ updateTransform() { const charge = this.charge; const current = charge.circuitElement.currentProperty.get(); NODE_MATRIX.set( charge.matrix ); if ( charge.charge > 0 ) { // Rotate if current is running backwards ( current < 0 ) && NODE_MATRIX.multiplyMatrix( HALF_ROTATION ); // Center NODE_MATRIX.multiplyMatrix( ARROW_OFFSET ); // Apply the transform this.matrix = NODE_MATRIX; let opacity = Util.linear( 0.015, CONVENTIONAL_CHARGE_THRESHOLD, 1, 0, Math.abs( charge.circuitElement.currentProperty.get() ) ); opacity = Util.clamp( opacity, 0, 1 ); this.setImageOpacity( opacity ); } else { // Set rotation to 0 since electrons should always be upside-up NODE_MATRIX.set00( 1 ); NODE_MATRIX.set01( 0 ); NODE_MATRIX.set10( 0 ); NODE_MATRIX.set11( 1 ); // Center the electrons NODE_MATRIX.multiplyMatrix( ELECTRON_OFFSET ); // Apply the transform this.matrix = NODE_MATRIX; } this.updateVisible(); this.outsideOfBlackBoxProperty.set( !charge.circuitElement.insideTrueBlackBoxProperty.get() ); } /** * @private - update the visibility */ updateVisible() { this.visible = this.charge.visibleProperty.get() && this.outsideOfBlackBoxProperty.get(); } } /** * Identifies the images used to render this node so they can be prepopulated in the WebGL sprite sheet. * @public {Array.<Image>} */ ChargeNode.webglSpriteNodes = [ ELECTRON_CHARGE_NODE, ARROW_NODE ]; return circuitConstructionKitCommon.register( 'ChargeNode', ChargeNode ); } );
intersection: function( ray ) { var self = this; var result = []; // find the rotation that will put our ray in the direction of the x-axis so we can only solve for y=0 for intersections var inverseMatrix = Matrix3.rotation2( -ray.direction.angle() ).timesMatrix( Matrix3.translation( -ray.position.x, -ray.position.y ) ); var p0 = inverseMatrix.timesVector2( this._start ); var p1 = inverseMatrix.timesVector2( this._control ); var p2 = inverseMatrix.timesVector2( this._end ); //(1-t)^2 start + 2(1-t)t control + t^2 end var a = p0.y - 2 * p1.y + p2.y; var b = -2 * p0.y + 2 * p1.y; var c = p0.y; var ts = solveQuadraticRootsReal( a, b, c ); _.each( ts, function( t ) { if ( t >= 0 && t <= 1 ) { var hitPoint = self.positionAt( t ); var unitTangent = self.tangentAt( t ).normalized(); var perp = unitTangent.perpendicular(); var toHit = hitPoint.minus( ray.position ); // make sure it's not behind the ray if ( toHit.dot( ray.direction ) > 0 ) { result.push( { distance: toHit.magnitude(), point: hitPoint, normal: perp.dot( ray.direction ) > 0 ? perp.negated() : perp, wind: ray.direction.perpendicular().dot( unitTangent ) < 0 ? 1 : -1 } ); } } } ); return result; },
updateDOM: function() { var node = this.node; var fillElement = this.fillElement; var strokeElement = this.strokeElement; if ( this.paintDirty ) { var borderRadius = Math.min( node._cornerXRadius, node._cornerYRadius ); var borderRadiusDirty = this.dirtyCornerXRadius || this.dirtyCornerYRadius; if ( this.dirtyWidth ) { fillElement.style.width = node._rectWidth + 'px'; } if ( this.dirtyHeight ) { fillElement.style.height = node._rectHeight + 'px'; } if ( borderRadiusDirty ) { fillElement.style[ Features.borderRadius ] = borderRadius + 'px'; // if one is zero, we are not rounded, so we do the min here } if ( this.dirtyFill ) { fillElement.style.backgroundColor = node.getCSSFill(); } if ( this.dirtyStroke ) { // update stroke presence if ( node.hasStroke() ) { strokeElement.style.borderStyle = 'solid'; } else { strokeElement.style.borderStyle = 'none'; } } if ( node.hasStroke() ) { // since we only execute these if we have a stroke, we need to redo everything if there was no stroke previously. // the other option would be to update stroked information when there is no stroke (major performance loss for fill-only rectangles) var hadNoStrokeBefore = !this.hadStroke; if ( hadNoStrokeBefore || this.dirtyWidth || this.dirtyLineWidth ) { strokeElement.style.width = ( node._rectWidth - node.getLineWidth() ) + 'px'; } if ( hadNoStrokeBefore || this.dirtyHeight || this.dirtyLineWidth ) { strokeElement.style.height = ( node._rectHeight - node.getLineWidth() ) + 'px'; } if ( hadNoStrokeBefore || this.dirtyLineWidth ) { strokeElement.style.left = ( -node.getLineWidth() / 2 ) + 'px'; strokeElement.style.top = ( -node.getLineWidth() / 2 ) + 'px'; strokeElement.style.borderWidth = node.getLineWidth() + 'px'; } if ( hadNoStrokeBefore || this.dirtyStroke ) { strokeElement.style.borderColor = node.getSimpleCSSStroke(); } if ( hadNoStrokeBefore || borderRadiusDirty || this.dirtyLineWidth || this.dirtyLineOptions ) { strokeElement.style[ Features.borderRadius ] = ( node.isRounded() || node.getLineJoin() === 'round' ) ? ( borderRadius + node.getLineWidth() / 2 ) + 'px' : '0'; } } } // shift the element vertically, postmultiplied with the entire transform. if ( this.transformDirty || this.dirtyX || this.dirtyY ) { scratchMatrix.set( this.getTransformMatrix() ); var translation = Matrix3.translation( node._rectX, node._rectY ); scratchMatrix.multiplyMatrix( translation ); translation.freeToPool(); scenery.Util.applyPreparedTransform( scratchMatrix, this.fillElement, this.forceAcceleration ); } // clear all of the dirty flags this.setToCleanState(); this.cleanPaintableState(); this.transformDirty = false; },