Skip to content

Commit

Permalink
simplify changeNucleonType and color animations (use interpolateRGBA …
Browse files Browse the repository at this point in the history
…instead of colorGradientIndexNumberProperty), phetsims/build-a-nucleus#85
  • Loading branch information
zepumph committed Aug 2, 2023
1 parent d8d8a05 commit 4acc8a1
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 102 deletions.
23 changes: 19 additions & 4 deletions js/model/Particle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,24 @@ import ShredConstants from '../ShredConstants.js';
import optionize from '../../../phet-core/js/optionize.js';
import TProperty from '../../../axon/js/TProperty.js';
import Property from '../../../axon/js/Property.js';
import { Color, ColorProperty } from '../../../scenery/js/imports.js';
import PhetColorScheme from '../../../scenery-phet/js/PhetColorScheme.js';

// used to give each particle a unique ID
let nextParticleId = 1;

// constants
const DEFAULT_PARTICLE_VELOCITY = 200; // Basically in pixels/sec.

// map of particle type to color information
export const PARTICLE_COLORS: Record<ParticleTypeString, Color> = {
proton: PhetColorScheme.RED_COLORBLIND,
neutron: Color.GRAY,
electron: Color.BLUE,
positron: Color.GREEN,
Isotope: Color.BLACK
};

export type ParticleTypeString = 'proton' | 'neutron' | 'electron' | 'positron' | 'Isotope';

type SelfOptions = {
Expand All @@ -53,9 +64,10 @@ class Particle extends PhetioObject {

public readonly inputEnabledProperty: TProperty<boolean> = new BooleanProperty( true );

// Keep track of the index in the nucleonColorChange gradient. The default is 4 since there are 5 colors
// in the color gradient array. See the NUCLEON_COLOR_GRADIENT array in ParticleNode.js.
public readonly colorGradientIndexNumberProperty = new NumberProperty( 4 );

// The "base" color of the Particle, dependent on what type of Particle it is. We say "base" because there is a color
// gradient applied to present a 3D graphic.
public readonly colorProperty: TProperty<Color>;
public readonly positionProperty: TProperty<Vector2>;
public readonly destinationProperty: TProperty<Vector2>;
public readonly radiusProperty: TProperty<number>;
Expand All @@ -82,6 +94,9 @@ class Particle extends PhetioObject {

this.typeProperty = new Property<ParticleTypeString>( type );

// Can be changed in rare cases, see ParticleAtom.changeNucleonType()
this.colorProperty = new ColorProperty( PARTICLE_COLORS[ type ] );

this.positionProperty = new Vector2Property( Vector2.ZERO, {
valueComparisonStrategy: 'equalsFunction',
tandem: options.tandem && options.tandem.createTandem( 'positionProperty' )
Expand Down Expand Up @@ -121,7 +136,7 @@ class Particle extends PhetioObject {

this.disposeParticle = () => {
this.typeProperty.dispose();
this.colorGradientIndexNumberProperty.dispose();
this.colorProperty.dispose();
this.positionProperty.dispose();
this.destinationProperty.dispose();
this.radiusProperty.dispose();
Expand Down
59 changes: 16 additions & 43 deletions js/model/ParticleAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import AtomIdentifier from '../AtomIdentifier.js';
import shred from '../shred.js';
import ShredConstants from '../ShredConstants.js';
import Utils from '../Utils.js';
import Particle, { ParticleTypeString } from './Particle.js';
import Particle, { PARTICLE_COLORS, ParticleTypeString } from './Particle.js';
import optionize from '../../../phet-core/js/optionize.js';
import TProperty from '../../../axon/js/TProperty.js';
import TReadOnlyProperty from '../../../axon/js/TReadOnlyProperty.js';
Expand All @@ -38,15 +38,6 @@ import ReadOnlyProperty from '../../../axon/js/ReadOnlyProperty.js';
// constants
const NUM_ELECTRON_POSITIONS = 10; // first two electron shells, i.e. 2 + 8

// color gradient between the color of a proton and a neutron
const NUCLEON_COLOR_GRADIENT = [
PhetColorScheme.RED_COLORBLIND,
new Color( '#e06020' ), // 1/4 point
new Color( '#c06b40' ), // half-way point
new Color( '#a07660' ), // 3/4 point
Color.GRAY
];

// helper function for retrieving the tandem for a particle
const ParticleReferenceIO = ReferenceIO( Particle.ParticleIO );
const NullableParticleReferenceIO = NullableIO( ReferenceIO( Particle.ParticleIO ) );
Expand Down Expand Up @@ -625,62 +616,44 @@ class ParticleAtom extends PhetioObject {
/**
* Change the nucleon type of the provided particle to the other nucleon type.
*/
public changeNucleonType( particle: Particle, onChangeComplete: VoidFunction ): Animation {
public changeNucleonType( particle: Particle, onChangeComplete: VoidFunction ): void {
assert && assert( this.containsParticle( particle ), 'ParticleAtom does not contain this particle ' + particle.id );
assert && assert( particle.type === 'proton' || particle.type === 'neutron', 'Particle type must be a proton or a neutron.' );

const isParticleTypeProton = particle.type === 'proton';
const oldParticleArray = isParticleTypeProton ? this.protons : this.neutrons;
const newParticleArray = isParticleTypeProton ? this.neutrons : this.protons;

particle.typeProperty.value = ( isParticleTypeProton ? 'neutron' : 'proton' ) as ParticleTypeString;
const newParticleType = isParticleTypeProton ? 'neutron' : 'proton';
particle.typeProperty.value = newParticleType as ParticleTypeString;

const particleType = particle.typeProperty.value;

let nucleonChangeColorChange: Color[];
if ( particleType === 'proton' ) {
nucleonChangeColorChange = NUCLEON_COLOR_GRADIENT.slice().reverse();
}
else if ( particleType === 'neutron' ) {
nucleonChangeColorChange = NUCLEON_COLOR_GRADIENT.slice();
}
else {
assert && assert( false, `unsupported particle type: ${particleType}` );
}
assert && assert( particleType === 'proton' || particleType === 'neutron',
'can only change type between protons and neutrons' );

// Animate through the values in nucleonColorChange to 'slowly' change the color of the nucleon.
const initialColorChangeAnimation = new Animation( {
from: particle.colorGradientIndexNumberProperty.initialValue,
to: 1,
setValue: indexValue => { particle.colorGradientIndexNumberProperty.value = indexValue; },
duration: 0.1,
easing: Easing.LINEAR
} );
const startingColor = particle.colorProperty.value;
const targetColor = PARTICLE_COLORS[ newParticleType ];

const finalColorChangeAnimation = new Animation( {
from: 1,
to: nucleonChangeColorChange!.length - 1,
setValue: indexValue => { particle.colorGradientIndexNumberProperty.value = indexValue; },
duration: 0.4,
const colorChangeAnimation = new Animation( {
from: 0,
to: 1,
setValue: indexValue => { particle.colorProperty.value = Color.interpolateRGBA( startingColor, targetColor, indexValue ); },
duration: 0.5,
easing: Easing.LINEAR
} );

this.liveAnimations.push( initialColorChangeAnimation );
this.liveAnimations.push( finalColorChangeAnimation );
this.liveAnimations.push( colorChangeAnimation );

initialColorChangeAnimation.then( finalColorChangeAnimation );
initialColorChangeAnimation.start();

initialColorChangeAnimation.finishEmitter.addListener( () => onChangeComplete() );
colorChangeAnimation.finishEmitter.addListener( () => onChangeComplete() );
colorChangeAnimation.start();

// Defer the massNumberProperty links until the particle arrays are correct so the nucleus does not reconfigure.
const wasDeferred = this.massNumberProperty.isDeferred;
this.massNumberProperty.setDeferred( true );
arrayRemove( oldParticleArray, particle );
newParticleArray.push( particle );
!wasDeferred && this.massNumberProperty.setDeferred( false );

return initialColorChangeAnimation;
}

// This function was only created to support flexibility in the "numberAtom" parameter for PeriodicTableNode, use carefully.
Expand Down
70 changes: 16 additions & 54 deletions js/view/ParticleNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,24 @@
* track a particle, use ParticleView for that. Basically this is just an icon.
*/

import Utils from '../../../dot/js/Utils.js';
import PhetColorScheme from '../../../scenery-phet/js/PhetColorScheme.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 { PARTICLE_COLORS, 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;
const HIGH_CONTRAST_LINE_WIDTH = 2;

// map of particle type to color information
const PARTICLE_COLORS: Record<ParticleTypeString, Color> = {
proton: PhetColorScheme.RED_COLORBLIND,
neutron: Color.GRAY,
electron: Color.BLUE,
positron: Color.GREEN,
Isotope: Color.BLACK
};

// color gradient between the color of a proton and a neutron
const NUCLEON_COLOR_GRADIENT = [
PARTICLE_COLORS.proton,
new Color( '#e06020' ), // 1/4 point
new Color( '#c06b40' ), // half-way point
new Color( '#a07660' ), // 3/4 point
PARTICLE_COLORS.neutron
];


type SelfOptions = {

// {BooleanProperty|null} - if provided, this is used to set the particle node into and out of high contrast mode
highContrastProperty?: TReadOnlyProperty<boolean>;
typeProperty?: TReadOnlyProperty<ParticleTypeString> | null;
colorGradientIndexNumberProperty?: TReadOnlyProperty<number> | null;
colorProperty?: TReadOnlyProperty<Color>;
};
type ParticleNodeOptions = SelfOptions & CircleOptions;

Expand All @@ -52,7 +31,13 @@ class ParticleNode extends Circle {

public constructor( particleType: ParticleTypeString, radius: number, providedOptions?: ParticleNodeOptions ) {

// Get the color to use as the basis for the gradients, fills, strokes and such.
const baseColor = PARTICLE_COLORS[ particleType ];
assert && assert( baseColor, `Unrecognized particle type: ${particleType}` );

const ownsHighContrastProperty = providedOptions && !providedOptions.highContrastProperty;
const ownsColorProperty = providedOptions && !providedOptions.colorProperty;

const options = optionize<ParticleNodeOptions, SelfOptions, CircleOptions>()( {

cursor: 'pointer',
Expand All @@ -61,61 +46,38 @@ class ParticleNode extends Circle {

typeProperty: null,

colorGradientIndexNumberProperty: null
colorProperty: new ColorProperty( baseColor )
}, providedOptions );

assert && assert( options.fill === undefined, 'fill will be set programmatically and should not be specified' );
assert && assert( options.stroke === undefined, 'stroke will be set programmatically and should not be specified' );
assert && assert( options.lineWidth === undefined, 'line width will be set programmatically and should not be specified' );

// Get the color to use as the basis for the gradients, fills, strokes and such.
const baseColor = PARTICLE_COLORS[ particleType ];
assert && assert( baseColor, `Unrecognized particle type: ${particleType}` );

const colorProperty = new ColorProperty( baseColor );
super( radius, options );

const colorMultilink = Multilink.multilink( [
colorProperty,
options.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 )
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, color );

// Set the options for the default look.
const nonHighContrastStroke = color.colorUtilsDarker( 0.33 );
this.fill = highContrast ? colorProperty.value : gradientFill;
this.stroke = highContrast ? colorProperty.value.colorUtilsDarker( 0.5 ) : nonHighContrastStroke;
this.fill = highContrast ? options.colorProperty.value : gradientFill;
this.stroke = highContrast ? options.colorProperty.value.colorUtilsDarker( 0.5 ) : nonHighContrastStroke;
this.lineWidth = highContrast ? HIGH_CONTRAST_LINE_WIDTH : DEFAULT_LINE_WIDTH;
} );


// change the color of the particle
options.colorGradientIndexNumberProperty && options.colorGradientIndexNumberProperty.link( indexValue => {
const typeProperty = options.typeProperty!;
if ( typeProperty.value === 'proton' || typeProperty.value === 'neutron' ) {

let nucleonChangeColorChange: Color[] = [ Color.BLACK ];
if ( typeProperty.value === 'proton' ) {
nucleonChangeColorChange = NUCLEON_COLOR_GRADIENT.slice().reverse();
}
else if ( typeProperty.value === 'neutron' ) {
nucleonChangeColorChange = NUCLEON_COLOR_GRADIENT.slice();
}

// the value is close to an integer
if ( Math.floor( indexValue * 10 ) / 10 % 1 === 0 ) {
colorProperty.value = nucleonChangeColorChange[ Utils.toFixed( indexValue, 0 ) as unknown as number ];
}
}
} );

this.disposeParticleNode = () => {
colorMultilink.dispose();
ownsHighContrastProperty && options.highContrastProperty.dispose();
ownsColorProperty && options.colorProperty.dispose();
};
}

Expand Down
2 changes: 1 addition & 1 deletion js/view/ParticleView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ function createParticleNode( particle: Particle, modelViewTransform: ModelViewTr
{
highContrastProperty: highContrastProperty,
typeProperty: particle.typeProperty,
colorGradientIndexNumberProperty: particle.colorGradientIndexNumberProperty,
colorProperty: particle.colorProperty,
tandem: tandem
}
);
Expand Down

0 comments on commit 4acc8a1

Please sign in to comment.