diff --git a/js/resistance-in-a-wire/view/DotsCanvasNode.js b/js/resistance-in-a-wire/view/DotsCanvasNode.js new file mode 100644 index 0000000..e1503bd --- /dev/null +++ b/js/resistance-in-a-wire/view/DotsCanvasNode.js @@ -0,0 +1,120 @@ +// Copyright 2018, University of Colorado Boulder + +/** + * Draw the dots in the wire with a CanvasNode as a performance enhancement. This is much faster than drawing drawing + * each dot as a Circle node and updating their visibility. Dots are redrawn whenever model Properties change that + * might change the shape of the wire or add or remove dots (length, area, resistivity). + * + * @author Jesse Greenberg + */ +define( function( require ) { + 'use strict'; + + // modules + var CanvasNode = require( 'SCENERY/nodes/CanvasNode' ); + var inherit = require( 'PHET_CORE/inherit' ); + var LinearFunction = require( 'DOT/LinearFunction' ); + var resistanceInAWire = require( 'RESISTANCE_IN_A_WIRE/resistanceInAWire' ); + var ResistanceInAWireConstants = require( 'RESISTANCE_IN_A_WIRE/resistance-in-a-wire/ResistanceInAWireConstants' ); + var Vector2 = require( 'DOT/Vector2' ); + var Bounds2 = require( 'DOT/Bounds2' ); + var WireShapeConstants = require( 'RESISTANCE_IN_A_WIRE/resistance-in-a-wire/view/WireShapeConstants' ); + + // constants + var DOT_RADIUS = 2; + + // to calculate the number of dots + var MAX_WIDTH_INCLUDING_ROUNDED_ENDS = WireShapeConstants.WIRE_VIEW_WIDTH_RANGE.max + 2 * WireShapeConstants.WIRE_VIEW_HEIGHT_RANGE.max * WireShapeConstants.PERSPECTIVE_FACTOR; + var AREA_PER_DOT = 200; // Adjust this to control the density of the dots. + var NUMBER_OF_DOTS = MAX_WIDTH_INCLUDING_ROUNDED_ENDS * WireShapeConstants.WIRE_VIEW_HEIGHT_RANGE.max / AREA_PER_DOT; + + // Function to map resistivity to number of dots. + var resistivityToNumberOfDots = new LinearFunction( + ResistanceInAWireConstants.RESISTIVITY_RANGE.min, + ResistanceInAWireConstants.RESISTIVITY_RANGE.max, + NUMBER_OF_DOTS * 0.05, + NUMBER_OF_DOTS, + true + ); + + /** + * @constructor + * @param {Bounds2} bounds - total bounds for the canvas + * @param {Object} [options] + */ + function DotsCanvasNode( model, options ) { + + options = _.extend( { + preventFit: true // don't recompute bounds as a performance enhancement + }, options ); + + // @private - Locations for dots randomly on the wire. Density is based on AREA_PER_DOT. + this.dotCenters = []; + for ( var i = 0; i < NUMBER_OF_DOTS; i++ ) { + var centerX = ( phet.joist.random.nextDouble() - .5 ) * MAX_WIDTH_INCLUDING_ROUNDED_ENDS; + var centerY = ( phet.joist.random.nextDouble() - .5 ) * WireShapeConstants.WIRE_VIEW_HEIGHT_RANGE.max; + this.dotCenters.push( new Vector2( centerX, centerY ) ); + } + + // @private - just for use in paintCanvas + this.resistivityProperty = model.resistivityProperty; + this.areaProperty = model.areaProperty; + this.lengthProperty = model.lengthProperty; + + // @public - dots outside of this area will be invisible, set when shape is recalculated in WireNode + this.dotsClipArea = null; + + // calculate bounds for the canvas - wire center is at (0, 0) + var height = WireShapeConstants.areaToHeight( ResistanceInAWireConstants.AREA_RANGE.max ); + var width = WireShapeConstants.lengthToWidth( ResistanceInAWireConstants.LENGTH_RANGE.max ); + var dotsBounds = new Bounds2( -width / 2 - WireShapeConstants.PERSPECTIVE_FACTOR * height / 2, -height / 2, width / 2 + WireShapeConstants.PERSPECTIVE_FACTOR * height / 2, height / 2 ); + + CanvasNode.call( this, options ); + this.setCanvasBounds( dotsBounds ); + this.invalidatePaint(); + } + + resistanceInAWire.register( 'DotsCanvasNode', DotsCanvasNode ); + + return inherit( CanvasNode, DotsCanvasNode, { + + /** + * Draw the required dots. + * + * @param {CanvasRenderingContext2D} context + * @override + * @public + */ + paintCanvas: function( context ) { + + // Height of the wire in view coordinates + var height = WireShapeConstants.areaToHeight( this.areaProperty.get() ); + + // Width of the wire (as measured from the top of the wire, that is excluding the rounding bits in the middle). + var width = WireShapeConstants.lengthToWidth( this.lengthProperty.get() ); + + // Rectangular shape of the body excluding the arcs at the ends, used as the clip area. Using and changing clip + // area that includes the arcs at the end of the wire is too slow. But using a clip area for the main body of the + // wire looks nice, and allows dots to be seen partially when the wire is at is minimum height. + context.beginPath(); + context.moveTo( -width / 2 - WireShapeConstants.PERSPECTIVE_FACTOR * height / 2, height / 2 ); + context.lineTo( width / 2 + WireShapeConstants.PERSPECTIVE_FACTOR * height / 2, height / 2 ); + context.lineTo( width / 2 + WireShapeConstants.PERSPECTIVE_FACTOR * height / 2, -height / 2 ); + context.lineTo( -width / 2 - WireShapeConstants.PERSPECTIVE_FACTOR * height / 2, -height / 2 ); + context.clip(); + + // draw dots whose centers are within the shape of the wire + var numDotsToShow = resistivityToNumberOfDots( this.resistivityProperty.get() ); + for ( var i = 0; i < this.dotCenters.length; i++ ) { + + // only draw dots that are within the current shape of the wire to clip dots that extend beyond + // the left and the right of the rectangular wire shape + if ( i < numDotsToShow && this.dotsClipArea.containsPoint( this.dotCenters[ i ] ) ) { + context.beginPath(); + context.arc( this.dotCenters[ i ].x, this.dotCenters[ i ].y, DOT_RADIUS, 0, 2 * Math.PI, true ); + context.fill(); + } + } + } + } ); +} ); diff --git a/js/resistance-in-a-wire/view/WireNode.js b/js/resistance-in-a-wire/view/WireNode.js index b316be6..a4a8d6e 100644 --- a/js/resistance-in-a-wire/view/WireNode.js +++ b/js/resistance-in-a-wire/view/WireNode.js @@ -11,38 +11,24 @@ define( function( require ) { 'use strict'; // modules - var Circle = require( 'SCENERY/nodes/Circle' ); + var DotsCanvasNode = require( 'RESISTANCE_IN_A_WIRE/resistance-in-a-wire/view/DotsCanvasNode' ); var inherit = require( 'PHET_CORE/inherit' ); - var LinearFunction = require( 'DOT/LinearFunction' ); var LinearGradient = require( 'SCENERY/util/LinearGradient' ); var Node = require( 'SCENERY/nodes/Node' ); var Path = require( 'SCENERY/nodes/Path' ); var platform = require( 'PHET_CORE/platform' ); var Property = require( 'AXON/Property' ); - var Range = require( 'DOT/Range' ); var resistanceInAWire = require( 'RESISTANCE_IN_A_WIRE/resistanceInAWire' ); var ResistanceInAWireA11yStrings = require( 'RESISTANCE_IN_A_WIRE/resistance-in-a-wire/ResistanceInAWireA11yStrings' ); var ResistanceInAWireConstants = require( 'RESISTANCE_IN_A_WIRE/resistance-in-a-wire/ResistanceInAWireConstants' ); var Shape = require( 'KITE/Shape' ); var StringUtils = require( 'PHETCOMMON/util/StringUtils' ); var Util = require( 'DOT/Util' ); + var WireShapeConstants = require( 'RESISTANCE_IN_A_WIRE/resistance-in-a-wire/view/WireShapeConstants' ); // a11y strings var wireDescriptionPatternString = ResistanceInAWireA11yStrings.wireDescriptionPatternString.value; - // constants - var PERSPECTIVE_FACTOR = 0.4; // Multiplier that controls the width of the ellipses on the ends of the wire. - var DOT_RADIUS = 2; - - // Used to calculate the size of the wire in screen coordinates from the model values - var WIRE_DIAMETER_MAX = Math.sqrt( ResistanceInAWireConstants.AREA_RANGE.max / Math.PI ) * 2; - var WIRE_VIEW_WIDTH_RANGE = new Range( 15, 500 ); // in screen coordinates - var WIRE_VIEW_HEIGHT_RANGE = new Range( 3, 180 ); // in screen coordinates - - var MAX_WIDTH_INCLUDING_ROUNDED_ENDS = WIRE_VIEW_WIDTH_RANGE.max + 2 * WIRE_VIEW_HEIGHT_RANGE.max * PERSPECTIVE_FACTOR; - var AREA_PER_DOT = 200; // Adjust this to control the density of the dots. - var NUMBER_OF_DOTS = MAX_WIDTH_INCLUDING_ROUNDED_ENDS * WIRE_VIEW_HEIGHT_RANGE.max / AREA_PER_DOT; - /** * The position is set using center values since this can grow or shrink in width and height as the area and length of * the wire changes. @@ -89,74 +75,30 @@ define( function( require ) { this.addChild( wireBody ); this.addChild( wireEnd ); - /** - * Transform to map the area to the height of the wire. - * @param {number} area - * @returns {number} - the height in screen coordinates - */ - var areaToHeight = function( area ) { - var radius_squared = area / Math.PI; - var diameter = Math.sqrt( radius_squared ) * 2; // radius to diameter - return WIRE_VIEW_HEIGHT_RANGE.max / WIRE_DIAMETER_MAX * diameter; - }; - - // Linear mapping transform - var lengthToWidth = new LinearFunction( - ResistanceInAWireConstants.LENGTH_RANGE.min, - ResistanceInAWireConstants.LENGTH_RANGE.max, - WIRE_VIEW_WIDTH_RANGE.min, - WIRE_VIEW_WIDTH_RANGE.max, - true ); - - // Create a container node for the dots, and the tandems to go along with it. - var dotsNodeTandem = tandem.createTandem( 'dotsNode' ); - var dotsNode = new Node( { tandem: dotsNodeTandem } ); - var dotsGroupTandem = dotsNodeTandem.createGroupTandem( 'dots' ); - - // Create the dots randomly on the wire. Density is based on AREA_PER_DOT. - for ( var i = 0; i < NUMBER_OF_DOTS; i++ ) { - - var centerX = ( phet.joist.random.nextDouble() - .5 ) * MAX_WIDTH_INCLUDING_ROUNDED_ENDS; - var centerY = ( phet.joist.random.nextDouble() - .5 ) * WIRE_VIEW_HEIGHT_RANGE.max; - var dot = new Circle( DOT_RADIUS, { - fill: 'black', - centerX: centerX, - centerY: centerY, - tandem: dotsGroupTandem.createNextTandem() - } ); - dotsNode.addChild( dot ); - } + // all dots representing resistivity + var dotsNode = new DotsCanvasNode( model ); this.addChild( dotsNode ); - - // Function to map resistivity to number of dots. - var resistivityToNumberOfDots = new LinearFunction( - ResistanceInAWireConstants.RESISTIVITY_RANGE.min, - ResistanceInAWireConstants.RESISTIVITY_RANGE.max, - NUMBER_OF_DOTS * 0.05, - NUMBER_OF_DOTS, - true - ); - + // Update the resistor on change. No need to unlink, as it is present for the lifetime of the sim. var self = this; Property.multilink( [ model.areaProperty, model.lengthProperty, model.resistivityProperty ], function( area, length, resistivity ) { // Height of the wire in view coordinates - var height = areaToHeight( area ); + var height = WireShapeConstants.areaToHeight( area ); // Width of the wire (as measured from the top of the wire, that is excluding the rounding bits in the middle). - var width = lengthToWidth( length ); + var width = WireShapeConstants.lengthToWidth( length ); // Set the (face) body shape of the wire. // Recall that (zero,zero) is defined as the center of the wire. wireBody.shape = new Shape().moveTo( -width / 2, height / 2 ) .horizontalLineToRelative( width ) - .ellipticalArc( width / 2, 0, PERSPECTIVE_FACTOR * height / 2, height / 2, 0, Math.PI / 2, 3 * Math.PI / 2, true ) + .ellipticalArc( width / 2, 0, WireShapeConstants.PERSPECTIVE_FACTOR * height / 2, height / 2, 0, Math.PI / 2, 3 * Math.PI / 2, true ) .horizontalLineToRelative( -width ); // Set the cap end of the wire - wireEnd.shape = Shape.ellipse( -width / 2, 0, height * PERSPECTIVE_FACTOR / 2, height / 2 ); + wireEnd.shape = Shape.ellipse( -width / 2, 0, height * WireShapeConstants.PERSPECTIVE_FACTOR / 2, height / 2 ); // Set the gradient on the wire to make it look more 3D. wireBody.fill = new LinearGradient( 0, height / 2, 0, -height / 2 ) @@ -166,14 +108,13 @@ define( function( require ) { .addColorStop( 0.8, '#F8E8D9' ) .addColorStop( 1, '#8C4828' ); - // Clip the dots that are shown to only include those inside the wire (including the wireEnd). - dotsNode.clipArea = wireBody.shape.ellipticalArc( -width / 2, 0, PERSPECTIVE_FACTOR * height / 2, height / 2, 0, 3 * Math.PI / 2, Math.PI / 2, true ); + // Clip the dots that are shown to only include those inside the wire (including the wireEnd). Don't use + // clipArea setter directly because it is too slow, but but the area is still used by + // DotsCanvasNode.paintCanvas + dotsNode.dotsClipArea = wireBody.shape.ellipticalArc( -width / 2, 0, WireShapeConstants.PERSPECTIVE_FACTOR * height / 2, height / 2, 0, 3 * Math.PI / 2, Math.PI / 2, true ); - // Set the number of visible dots based on the resistivity. - var numDotsToShow = resistivityToNumberOfDots( resistivity ); - dotsNode.children.forEach( function( dot, index ) { - dot.visible = index < numDotsToShow; - } ); + // redraw the dots representing resistivity + dotsNode.invalidatePaint(); self.accessibleDescription = self.getWireDescription(); } diff --git a/js/resistance-in-a-wire/view/WireShapeConstants.js b/js/resistance-in-a-wire/view/WireShapeConstants.js new file mode 100644 index 0000000..57efc8d --- /dev/null +++ b/js/resistance-in-a-wire/view/WireShapeConstants.js @@ -0,0 +1,57 @@ +// Copyright 2018, University of Colorado Boulder + +/** + * Collection of constants and functions that determine the visual shape of the Wire. + * + * @author Jesse Greenberg + */ + +define( function( require ) { + 'use strict'; + + // modules + var LinearFunction = require( 'DOT/LinearFunction' ); + var Range = require( 'DOT/Range' ); + var resistanceInAWire = require( 'RESISTANCE_IN_A_WIRE/resistanceInAWire' ); + var ResistanceInAWireConstants = require( 'RESISTANCE_IN_A_WIRE/resistance-in-a-wire/ResistanceInAWireConstants' ); + + // constants + var WIRE_VIEW_WIDTH_RANGE = new Range( 15, 500 ); // in screen coordinates + var WIRE_VIEW_HEIGHT_RANGE = new Range( 3, 180 ); // in screen coordinates + var WIRE_DIAMETER_MAX = Math.sqrt( ResistanceInAWireConstants.AREA_RANGE.max / Math.PI ) * 2; + + var WireShapeConstants = { + + // Multiplier that controls the width of the ellipses on the ends of the wire. + PERSPECTIVE_FACTOR: 0.4, + + // Used to calculate the size of the wire in screen coordinates from the model values + WIRE_DIAMETER_MAX: WIRE_DIAMETER_MAX, + WIRE_VIEW_WIDTH_RANGE: WIRE_VIEW_WIDTH_RANGE, + WIRE_VIEW_HEIGHT_RANGE: WIRE_VIEW_HEIGHT_RANGE, + + // Linear mapping transform + lengthToWidth: new LinearFunction( + ResistanceInAWireConstants.LENGTH_RANGE.min, + ResistanceInAWireConstants.LENGTH_RANGE.max, + WIRE_VIEW_WIDTH_RANGE.min, + WIRE_VIEW_WIDTH_RANGE.max, + true + ), + + /** + * Transform to map the area to the height of the wire. + * @param {number} area + * @returns {number} - the height in screen coordinates + */ + areaToHeight: function( area ) { + var radius_squared = area / Math.PI; + var diameter = Math.sqrt( radius_squared ) * 2; // radius to diameter + return WIRE_VIEW_HEIGHT_RANGE.max / WIRE_DIAMETER_MAX * diameter; + } + }; + + resistanceInAWire.register( 'WireShapeConstants', WireShapeConstants ); + + return WireShapeConstants; +} ); \ No newline at end of file