diff --git a/js/view/InteractiveSchematicAtom.ts b/js/view/InteractiveSchematicAtom.ts index 779ccfb..999ca75 100644 --- a/js/view/InteractiveSchematicAtom.ts +++ b/js/view/InteractiveSchematicAtom.ts @@ -16,11 +16,12 @@ import ParticleView from './ParticleView.js'; import TReadOnlyProperty from '../../../axon/js/TReadOnlyProperty.js'; import ModelViewTransform2 from '../../../phetcommon/js/view/ModelViewTransform2.js'; import optionize from '../../../phet-core/js/optionize.js'; +import BooleanProperty from '../../../axon/js/BooleanProperty.js'; // constants const NUM_NUCLEON_LAYERS = 5; // This is based on max number of particles, may need adjustment if that changes. type SelfOptions = { - highContrastProperty?: null | TReadOnlyProperty; + highContrastProperty?: TReadOnlyProperty; }; type InteractiveSchematicAtomOptions = SelfOptions & NodeOptions; @@ -29,10 +30,12 @@ class InteractiveSchematicAtom extends Node { private readonly disposeInteractiveSchematicAtom: VoidFunction; public constructor( model: BuildAnAtomModel, modelViewTransform: ModelViewTransform2, providedOptions?: InteractiveSchematicAtomOptions ) { + const ownsHighContrastProperty = providedOptions && !providedOptions.highContrastProperty; + const options = optionize()( { // {Property.|null} - property that can be used to turn on high-contrast particles - highContrastProperty: null, + highContrastProperty: new BooleanProperty( false ), tandem: Tandem.REQUIRED }, providedOptions ); @@ -156,6 +159,7 @@ class InteractiveSchematicAtom extends Node { atomNode.dispose(); model.particleAtom.electrons.lengthProperty.unlink( updateElectronVisibility ); model.electronShellDepictionProperty.unlink( updateElectronVisibility ); + ownsHighContrastProperty && options.highContrastProperty.dispose(); }; this.mutate( options ); diff --git a/js/view/ParticleNode.ts b/js/view/ParticleNode.ts index 1db0769..18edc31 100644 --- a/js/view/ParticleNode.ts +++ b/js/view/ParticleNode.ts @@ -7,11 +7,13 @@ import Utils from '../../../dot/js/Utils.js'; import PhetColorScheme from '../../../scenery-phet/js/PhetColorScheme.js'; -import { Circle, CircleOptions, Color, RadialGradient } from '../../../scenery/js/imports.js'; +import { Circle, CircleOptions, Color, ColorProperty, RadialGradient } from '../../../scenery/js/imports.js'; import shred from '../shred.js'; import TReadOnlyProperty from '../../../axon/js/TReadOnlyProperty.js'; import optionize from '../../../phet-core/js/optionize.js'; import { ParticleTypeString } from '../model/Particle.js'; +import BooleanProperty from '../../../axon/js/BooleanProperty.js'; +import Multilink from '../../../axon/js/Multilink.js'; // constants const DEFAULT_LINE_WIDTH = 0.5; @@ -39,7 +41,7 @@ const NUCLEON_COLOR_GRADIENT = [ type SelfOptions = { // {BooleanProperty|null} - if provided, this is used to set the particle node into and out of high contrast mode - highContrastProperty?: TReadOnlyProperty | null; + highContrastProperty?: TReadOnlyProperty; typeProperty?: TReadOnlyProperty | null; colorGradientIndexNumberProperty?: TReadOnlyProperty | null; }; @@ -50,11 +52,12 @@ class ParticleNode extends Circle { public constructor( particleType: ParticleTypeString, radius: number, providedOptions?: ParticleNodeOptions ) { + const ownsHighContrastProperty = providedOptions && !providedOptions.highContrastProperty; const options = optionize()( { cursor: 'pointer', - highContrastProperty: null, + highContrastProperty: new BooleanProperty( false ), typeProperty: null, @@ -69,35 +72,26 @@ class ParticleNode extends Circle { const baseColor = PARTICLE_COLORS[ particleType ]; assert && assert( baseColor, `Unrecognized particle type: ${particleType}` ); - // Create the fill that will be used to make the particles look 3D when not in high-contrast mode. - const gradientFill = new RadialGradient( -radius * 0.4, -radius * 0.4, 0, -radius * 0.4, -radius * 0.4, radius * 1.6 ) - .addColorStop( 0, 'white' ) - .addColorStop( 1, baseColor ); - - // Set the options for the default look. - const nonHighContrastStroke = baseColor.colorUtilsDarker( 0.33 ); - options.fill = gradientFill; - options.stroke = nonHighContrastStroke; - options.lineWidth = DEFAULT_LINE_WIDTH; - + const colorProperty = new ColorProperty( baseColor ); super( radius, options ); - // function to change the color of a particle - const changeParticleColor = ( newColor: Color ) => { + const colorMultilink = Multilink.multilink( [ + colorProperty, + options.highContrastProperty + ], ( color, highContrast ) => { // Create the fill that will be used to make the particles look 3D when not in high-contrast mode. const gradientFill = new RadialGradient( -radius * 0.4, -radius * 0.4, 0, -radius * 0.4, -radius * 0.4, radius * 1.6 ) .addColorStop( 0, 'white' ) - .addColorStop( 1, newColor ); + .addColorStop( 1, color ); // Set the options for the default look. - const nonHighContrastStroke = newColor.colorUtilsDarker( 0.33 ); + const nonHighContrastStroke = color.colorUtilsDarker( 0.33 ); + this.fill = highContrast ? colorProperty.value : gradientFill; + this.stroke = highContrast ? colorProperty.value.colorUtilsDarker( 0.5 ) : nonHighContrastStroke; + this.lineWidth = highContrast ? HIGH_CONTRAST_LINE_WIDTH : DEFAULT_LINE_WIDTH; + } ); - this.mutate( { - fill: gradientFill, - stroke: nonHighContrastStroke - } ); - }; // change the color of the particle options.colorGradientIndexNumberProperty && options.colorGradientIndexNumberProperty.link( indexValue => { @@ -114,27 +108,14 @@ class ParticleNode extends Circle { // the value is close to an integer if ( Math.floor( indexValue * 10 ) / 10 % 1 === 0 ) { - changeParticleColor( nucleonChangeColorChange[ Utils.toFixed( indexValue, 0 ) as unknown as number ] ); + colorProperty.value = nucleonChangeColorChange[ Utils.toFixed( indexValue, 0 ) as unknown as number ]; } } } ); - // If a highContrastProperty is provided, update the particle appearance based on its value. - // Set up the fill and the strokes based on whether the highContrastProperty option is provided. - let highContrastListener: ( ( highContrast: boolean ) => void ) | null = null; - if ( options.highContrastProperty ) { - highContrastListener = highContrast => { - this.fill = highContrast ? baseColor : gradientFill; - this.stroke = highContrast ? baseColor.colorUtilsDarker( 0.5 ) : nonHighContrastStroke; - this.lineWidth = highContrast ? HIGH_CONTRAST_LINE_WIDTH : DEFAULT_LINE_WIDTH; - }; - options.highContrastProperty.link( highContrastListener ); - } - this.disposeParticleNode = () => { - if ( highContrastListener ) { - options.highContrastProperty!.unlink( highContrastListener ); - } + colorMultilink.dispose(); + ownsHighContrastProperty && options.highContrastProperty.dispose(); }; } diff --git a/js/view/ParticleView.ts b/js/view/ParticleView.ts index 98b9215..aa79b24 100644 --- a/js/view/ParticleView.ts +++ b/js/view/ParticleView.ts @@ -1,7 +1,7 @@ // Copyright 2014-2023, University of Colorado Boulder /** - * Type that represents a sub-atomic particle in the view. + * Type that represents a subatomic particle in the view. */ import Property from '../../../axon/js/Property.js'; @@ -16,10 +16,11 @@ import ModelViewTransform2 from '../../../phetcommon/js/view/ModelViewTransform2 import optionize from '../../../phet-core/js/optionize.js'; import Vector2 from '../../../dot/js/Vector2.js'; import TReadOnlyProperty from '../../../axon/js/TReadOnlyProperty.js'; +import BooleanProperty from '../../../axon/js/BooleanProperty.js'; type SelfOptions = { dragBounds?: Bounds2; - highContrastProperty?: TReadOnlyProperty | null; + highContrastProperty?: TReadOnlyProperty; }; type ParticleViewOptions = SelfOptions & NodeOptions; @@ -30,12 +31,14 @@ class ParticleView extends Node { public constructor( particle: Particle, modelViewTransform: ModelViewTransform2, providedOptions?: ParticleViewOptions ) { + const ownsHighContrastProperty = providedOptions && !providedOptions.highContrastProperty; + const options = optionize()( { dragBounds: Bounds2.EVERYTHING, tandem: Tandem.REQUIRED, // {BooleanProperty|null} - if provided, this is used to set the particle node into and out of high contrast mode - highContrastProperty: null + highContrastProperty: new BooleanProperty( false ) }, providedOptions ); super(); @@ -98,6 +101,7 @@ class ParticleView extends Node { this.mutate( options ); this.disposeParticleView = function() { + ownsHighContrastProperty && options.highContrastProperty.dispose(); particle.positionProperty.unlink( updateParticlePosition ); particleNode.dispose(); this.dragListener.dispose(); @@ -120,7 +124,7 @@ class ParticleView extends Node { * Creates the proper view for a particle. */ function createParticleNode( particle: Particle, modelViewTransform: ModelViewTransform2, - highContrastProperty: TReadOnlyProperty | null, tandem: Tandem ): Node { + highContrastProperty: TReadOnlyProperty, tandem: Tandem ): Node { let particleNode; if ( particle.type === 'Isotope' ) { particleNode = new IsotopeNode(