Skip to content

Commit

Permalink
implement dots with CanvasNode for #165, cherry picked to 1.5 branch …
Browse files Browse the repository at this point in the history
…for #168
  • Loading branch information
jessegreenberg authored and zepumph committed Aug 23, 2018
1 parent 5f5018c commit b4b05a6
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 74 deletions.
120 changes: 120 additions & 0 deletions js/resistance-in-a-wire/view/DotsCanvasNode.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
} );
} );
89 changes: 15 additions & 74 deletions js/resistance-in-a-wire/view/WireNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 )
Expand All @@ -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();
}
Expand Down
57 changes: 57 additions & 0 deletions js/resistance-in-a-wire/view/WireShapeConstants.js
Original file line number Diff line number Diff line change
@@ -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;
} );

0 comments on commit b4b05a6

Please sign in to comment.