Skip to content

Commit

Permalink
Factor out ValveNode from PipeNode, see: #78
Browse files Browse the repository at this point in the history
  • Loading branch information
marlitas committed Aug 5, 2022
1 parent 7921319 commit e5be158
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 92 deletions.
98 changes: 6 additions & 92 deletions js/intro/view/PipeNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2022, University of Colorado Boulder

//REVIEW It would be advantageous to factor ValueNode out as a separate class.
/**
* Representation for the 2D pipe and valve between each water cup.
*
Expand All @@ -11,14 +10,15 @@
import Vector2 from '../../../../dot/js/Vector2.js';
import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js';
import optionize, { EmptySelfOptions } from '../../../../phet-core/js/optionize.js';
import { FireListener, LinearGradient, Node, NodeOptions, Path, RadialGradient, Rectangle } from '../../../../scenery/js/imports.js';
import { FireListener, LinearGradient, Node, NodeOptions, Rectangle } from '../../../../scenery/js/imports.js';
import meanShareAndBalance from '../../meanShareAndBalance.js';
import Pipe from '../model/Pipe.js';
import MeanShareAndBalanceConstants from '../../common/MeanShareAndBalanceConstants.js';
import MeanShareAndBalanceColors from '../../common/MeanShareAndBalanceColors.js';
import Bounds2 from '../../../../dot/js/Bounds2.js';
import { Shape } from '../../../../kite/js/imports.js';
import StrictOmit from '../../../../phet-core/js/types/StrictOmit.js';
import ValveNode from './ValveNode.js';

type SelfOptions = EmptySelfOptions;

Expand All @@ -27,19 +27,13 @@ type PipeNodeOptions = SelfOptions & StrictOmit<NodeOptions, 'phetioDynamicEleme
const VALVE_RADIUS = 8;
const PIPE_WIDTH = 4;
const HANDLE_HEIGHT = 10;
const HANDLE_WIDTH = 4;
const LINE_WIDTH = 1;

export default class PipeNode extends Node {
private readonly pipe: Pipe;
private readonly valveRotationFireListener: FireListener;
private readonly handleGrip: Path;
private readonly valveNode: Node;
private readonly pipeRectangle: Rectangle;
private readonly innerValve: Path;
private readonly outerValve: Path;
private readonly innerPipe: Rectangle;
private readonly handleBase: Rectangle;
private readonly valveNode: ValveNode;

public constructor( pipe: Pipe, modelViewTransform: ModelViewTransform2, providedOptions?: PipeNodeOptions ) {
const options = optionize<PipeNodeOptions, SelfOptions, NodeOptions>()( {
Expand All @@ -58,20 +52,6 @@ export default class PipeNode extends Node {
this.pipeRectangle = new Rectangle( 0, 0, MeanShareAndBalanceConstants.PIPE_LENGTH, PIPE_WIDTH,
{ stroke: 'black', fill: pipeGradient } );

// Function to create circle with center rectangle cut out.
const createInnerCircle = ( radius: number, rectangleWidth: number ): Shape => {
const circle = Shape.circle( radius - 1 );
const rectangle = Shape.rectangle( -rectangleWidth / 2, -radius - 5, rectangleWidth, ( radius + 5 ) * 2 );
return circle.shapeDifference( rectangle );
};

const createOuterCircle = ( radius: number ): Shape => {
const outerCircle = Shape.circle( radius + 2 );
const innerCircle = Shape.circle( radius - 1 );

return outerCircle.shapeDifference( innerCircle );
};

const outerValveDiameter = ( VALVE_RADIUS + 3 ) * 2;

// Base valve centering off of valve's 'open' position.
Expand All @@ -84,60 +64,10 @@ export default class PipeNode extends Node {
return clipAreaRectangle.shapeDifference( clipAreaCircle );
};

// Valve drawing
const valveGradient = new RadialGradient( 0, 0, 0, 0, 0, VALVE_RADIUS + 2 )
.addColorStop( 0.5, MeanShareAndBalanceColors.pipeGradientLightColorProperty )
.addColorStop( 1, MeanShareAndBalanceColors.pipeGradientDarkColorProperty );

this.innerValve = new Path( createInnerCircle( VALVE_RADIUS, PIPE_WIDTH ),
{ fill: 'black', lineWidth: LINE_WIDTH } );
this.outerValve = new Path( createOuterCircle( VALVE_RADIUS ),
{ fill: valveGradient, stroke: 'black', lineWidth: LINE_WIDTH } );

// Inner pipe shows water color when pipe is opened.
this.innerPipe = new Rectangle( 0, 0, PIPE_WIDTH, VALVE_RADIUS * 2, {
fill: null,
center: this.innerValve.center
} );

const handleGripGradient = new LinearGradient( -2, -2, 8, 0 )
.addColorStop( 0, MeanShareAndBalanceColors.handleGradientLightColorProperty )
.addColorStop( 0.4, MeanShareAndBalanceColors.handleGradientDarkColorProperty );

this.handleBase = new Rectangle( 0, 0, HANDLE_WIDTH, 3, {
fill: pipeGradient,
stroke: 'black',
lineWidth: LINE_WIDTH,
y: this.outerValve.top - 3,
x: this.innerValve.centerX - HANDLE_WIDTH / 2
} );

const handleShape = new Shape()
.moveTo( -2, 0 )
.lineTo( -4, -HANDLE_HEIGHT )
.ellipticalArc( 0, -HANDLE_HEIGHT, 4, 3, 0, Math.PI, 0, false )
.lineTo( 2, 0 )
.close();

this.handleGrip = new Path( handleShape, {
fill: handleGripGradient,
stroke: 'black',
lineWidth: LINE_WIDTH,
y: this.handleBase.top + 1
} );

// TODO: Sam please help me understand coordinate frames. The difference between setting center & x,y
this.valveNode = new Node( {
children: [ this.handleBase, this.handleGrip, this.innerPipe, this.outerValve, this.innerValve ],
cursor: 'pointer',
tandem: options.tandem?.createTandem( 'valveNode' ),
tagName: 'button',
y: pipeCenter.y,
x: pipeCenter.x + valveCenterOffset
} );

this.pipeRectangle.clipArea = createPipeClipArea( this.pipeRectangle.localBounds, VALVE_RADIUS );

this.valveNode = new ValveNode( pipeCenter, valveCenterOffset, pipeGradient, pipe.isOpenProperty, options.tandem );

// Set pointer areas for valveNode
this.valveNode.mouseArea = this.valveNode.localBounds.dilated( MeanShareAndBalanceConstants.MOUSE_AREA_DILATION );
this.valveNode.touchArea = this.valveNode.localBounds.dilated( MeanShareAndBalanceConstants.TOUCH_AREA_DILATION );
Expand Down Expand Up @@ -165,24 +95,8 @@ export default class PipeNode extends Node {
this.y = modelViewTransform.modelToViewY( 0 ) - PIPE_WIDTH;
}

// Valve animation
public step( dt: number ): void {

// TODO: Maybe move this to the model?
const currentRotation = this.valveNode.rotation;
const targetRotation = this.pipe.isOpenProperty.value ? Math.PI / 2 : 0;
const delta = targetRotation - currentRotation;
const rotationThreshold = Math.abs( this.valveNode.rotation - targetRotation ) * 0.4;
const proposedRotation = currentRotation + Math.sign( delta ) * dt * 3;
this.valveNode.rotation = rotationThreshold <= dt ? targetRotation : proposedRotation;

this.innerPipe.fill = this.valveNode.rotation >= ( Math.PI / 3 ) ? MeanShareAndBalanceColors.waterFillColorProperty : null;
}

public override dispose(): void {
super.dispose();
this.valveRotationFireListener.dispose();
this.valveNode.dispose();
this.valveNode.step( dt );
}
}

Expand Down
113 changes: 113 additions & 0 deletions js/intro/view/ValveNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2022, University of Colorado Boulder

/**
* Representation for the valve that opens and closes a pipe.
*
* @author Marla Schulz (PhET Interactive Simulations)
* @author Sam Reid (PhET Interactive Simulations)
*/

import Property from '../../../../axon/js/Property.js';
import Vector2 from '../../../../dot/js/Vector2.js';
import { Shape } from '../../../../kite/js/imports.js';
import { LinearGradient, Node, Path, RadialGradient, Rectangle } from '../../../../scenery/js/imports.js';
import Tandem from '../../../../tandem/js/Tandem.js';
import MeanShareAndBalanceColors from '../../common/MeanShareAndBalanceColors.js';
import meanShareAndBalance from '../../meanShareAndBalance.js';

const HANDLE_WIDTH = 4;
const HANDLE_HEIGHT = 10;
const LINE_WIDTH = 1;
const PIPE_WIDTH = 4;
const VALVE_RADIUS = 8;

export default class ValveNode extends Node {
private readonly isOpenProperty: Property<boolean>;
private readonly innerPipe: Rectangle;
public constructor( pipeCenter: Vector2, valveCenterOffset: number, pipeGradient: LinearGradient, isOpenProperty: Property<boolean>, tandem: Tandem ) {
// Valve drawing
const valveGradient = new RadialGradient( 0, 0, 0, 0, 0, VALVE_RADIUS + 2 )
.addColorStop( 0.5, MeanShareAndBalanceColors.pipeGradientLightColorProperty )
.addColorStop( 1, MeanShareAndBalanceColors.pipeGradientDarkColorProperty );

// Function to create circle with center rectangle cut out.
const createInnerCircle = ( radius: number, rectangleWidth: number ): Shape => {
const circle = Shape.circle( radius - 1 );
const rectangle = Shape.rectangle( -rectangleWidth / 2, -radius - 5, rectangleWidth, ( radius + 5 ) * 2 );
return circle.shapeDifference( rectangle );
};

const createOuterCircle = ( radius: number ): Shape => {
const outerCircle = Shape.circle( radius + 2 );
const innerCircle = Shape.circle( radius - 1 );

return outerCircle.shapeDifference( innerCircle );
};

const innerValve = new Path( createInnerCircle( VALVE_RADIUS, PIPE_WIDTH ),
{ fill: 'black', lineWidth: LINE_WIDTH } );
const outerValve = new Path( createOuterCircle( VALVE_RADIUS ),
{ fill: valveGradient, stroke: 'black', lineWidth: LINE_WIDTH } );

// Inner pipe shows water color when pipe is opened.
const innerPipe = new Rectangle( 0, 0, PIPE_WIDTH, VALVE_RADIUS * 2, {
fill: null,
center: innerValve.center
} );

const handleGripGradient = new LinearGradient( -2, -2, 8, 0 )
.addColorStop( 0, MeanShareAndBalanceColors.handleGradientLightColorProperty )
.addColorStop( 0.4, MeanShareAndBalanceColors.handleGradientDarkColorProperty );

const handleBase = new Rectangle( 0, 0, HANDLE_WIDTH, 3, {
fill: pipeGradient,
stroke: 'black',
lineWidth: LINE_WIDTH,
y: outerValve.top - 3,
x: innerValve.centerX - HANDLE_WIDTH / 2
} );

const handleShape = new Shape()
.moveTo( -2, 0 )
.lineTo( -4, -HANDLE_HEIGHT )
.ellipticalArc( 0, -HANDLE_HEIGHT, 4, 3, 0, Math.PI, 0, false )
.lineTo( 2, 0 )
.close();

const handleGrip = new Path( handleShape, {
fill: handleGripGradient,
stroke: 'black',
lineWidth: LINE_WIDTH,
y: handleBase.top + 1
} );

// TODO: Sam please help me understand coordinate frames. The difference between setting center & x,y
super( {
children: [ handleBase, handleGrip, innerPipe, outerValve, innerValve ],
cursor: 'pointer',
tagName: 'button',
y: pipeCenter.y,
x: pipeCenter.x + valveCenterOffset,
tandem: tandem.createTandem( 'ValveNode' )
} );

this.isOpenProperty = isOpenProperty;
this.innerPipe = innerPipe;
}

// Valve animation
public step( dt: number ): void {

// TODO: Maybe move this to the model?
const currentRotation = this.rotation;
const targetRotation = this.isOpenProperty.value ? Math.PI / 2 : 0;
const delta = targetRotation - currentRotation;
const rotationThreshold = Math.abs( this.rotation - targetRotation ) * 0.4;
const proposedRotation = currentRotation + Math.sign( delta ) * dt * 3;
this.rotation = rotationThreshold <= dt ? targetRotation : proposedRotation;

this.innerPipe.fill = this.rotation >= ( Math.PI / 3 ) ? MeanShareAndBalanceColors.waterFillColorProperty : null;
}
}

meanShareAndBalance.register( 'ValveNode', ValveNode );

0 comments on commit e5be158

Please sign in to comment.