Skip to content

Commit

Permalink
Material can subtype for its colors, and it can do its own densityRange
Browse files Browse the repository at this point in the history
#256

Signed-off-by: Michael Kauzmann <[email protected]>
  • Loading branch information
zepumph committed Jul 11, 2024
1 parent d166f87 commit 64ca76f
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 67 deletions.
8 changes: 5 additions & 3 deletions js/buoyancy/view/FluidDisplacedAccordionBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,16 @@ export default class FluidDisplacedAccordionBox extends AccordionBox {
const beakerVolumeProperty = new NumberProperty( 0, { range: BEAKER_RANGE.copy() } );

const solutionFillProperty = new DynamicProperty<Color, Color, Material>( fluidMaterialProperty, {
derive: material => material.liquidColor!,
derive: material => {
assert && assert( material.liquidColor, 'liquid color needed here' );
return material.liquidColor!;
},
map: color => {

// Below this threshold, use the same color for better contrast, see https://github.com/phetsims/buoyancy/issues/154
if ( fluidMaterialProperty.value.custom ) {

if ( fluidMaterialProperty.value.density < SAME_COLOR_MIN_DENSITY_THRESHOLD ) {
color = Material.getCustomLiquidColor( SAME_COLOR_MIN_DENSITY_THRESHOLD, DensityBuoyancyCommonConstants.FLUID_DENSITY_RANGE_PER_M3 ).value;
color = Material.getCustomLiquidColor( SAME_COLOR_MIN_DENSITY_THRESHOLD, DensityBuoyancyCommonConstants.FLUID_DENSITY_RANGE_PER_M3 );
}

return color.withAlpha( 0.8 );
Expand Down
138 changes: 79 additions & 59 deletions js/common/model/Material.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import TProperty from '../../../../axon/js/TProperty.js';
import Property from '../../../../axon/js/Property.js';
import Utils from '../../../../dot/js/Utils.js';
import ThreeUtils from '../../../../mobius/js/ThreeUtils.js';
import optionize, { combineOptions } from '../../../../phet-core/js/optionize.js';
import optionize, { combineOptions, EmptySelfOptions } from '../../../../phet-core/js/optionize.js';
import { Color, ColorProperty, ColorState } from '../../../../scenery/js/imports.js';
import BooleanIO from '../../../../tandem/js/types/BooleanIO.js';
import IOType from '../../../../tandem/js/types/IOType.js';
Expand All @@ -25,11 +25,11 @@ import DensityBuoyancyCommonColors from '../view/DensityBuoyancyCommonColors.js'
import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js';
import TinyProperty from '../../../../axon/js/TinyProperty.js';
import ReadOnlyProperty from '../../../../axon/js/ReadOnlyProperty.js';
import MappedProperty from '../../../../axon/js/MappedProperty.js';
import Range from '../../../../dot/js/Range.js';
import WithRequired from '../../../../phet-core/js/types/WithRequired.js';
import StrictOmit from '../../../../phet-core/js/types/StrictOmit.js';
import NumberProperty from '../../../../axon/js/NumberProperty.js';
import DerivedProperty from '../../../../axon/js/DerivedProperty.js';
import MappedProperty from '../../../../axon/js/MappedProperty.js';

const NullableColorPropertyReferenceType = NullableIO( ReferenceIO( Property.PropertyIO( Color.ColorIO ) ) );

Expand Down Expand Up @@ -108,6 +108,9 @@ export type MaterialOptions = {
// in SI (kg/m^3)
density?: number;

// What potential densities can this Material accept? (mostly applies to custom materials)
densityRange?: Range;

// in SI (Pa * s). For reference a poise is 1e-2 Pa*s, and a centipoise is 1e-3 Pa*s.
viscosity?: number;

Expand All @@ -117,6 +120,7 @@ export type MaterialOptions = {
// If true, don't show the density in number pickers/readouts
hidden?: boolean;

// TODO: Rename to customColorProperty, https://github.com/phetsims/density-buoyancy-common/issues/256
// TODO: Can we combine custom/liquid colors? https://github.com/phetsims/density-buoyancy-common/issues/256
// Uses the color for a solid material's color
customColor?: ReadOnlyProperty<Color> | null;
Expand All @@ -128,7 +132,6 @@ export type MaterialOptions = {
depthLinesColorProperty?: TReadOnlyProperty<Color>;
};
type NoIdentifierMaterialOptions = StrictOmit<MaterialOptions, 'identifier'>;
export type CreateCustomMaterialOptions = NoIdentifierMaterialOptions & Required<Pick<MaterialOptions, 'density'>> & { densityRange?: Range };

// TODO: Material should wire up color properties https://github.com/phetsims/density-buoyancy-common/issues/256
// TODO: Material only needs one freaking color Property, https://github.com/phetsims/density-buoyancy-common/issues/256
Expand All @@ -153,9 +156,9 @@ export default class Material {
// TODO: Eliminate custom as an orthogonal attribute, it can be determined from identifier. https://github.com/phetsims/density-buoyancy-common/issues/256
public readonly custom: boolean;
public readonly hidden: boolean;
public readonly customColor: ReadOnlyProperty<Color> | null;
public readonly liquidColor: ReadOnlyProperty<Color> | null;
public readonly depthLinesColorProperty: TReadOnlyProperty<Color>;
public customColor: ReadOnlyProperty<Color> | null;
public liquidColor: ReadOnlyProperty<Color> | null;
public depthLinesColorProperty: TReadOnlyProperty<Color>;
public readonly densityProperty: NumberProperty;

public constructor( providedOptions: MaterialOptions ) {
Expand All @@ -164,6 +167,7 @@ export default class Material {
nameProperty: new TinyProperty( 'unknown' ),
tandemName: null,
density: 1,
densityRange: new Range( 0.8, 23000 ),
viscosity: 1e-3,
custom: false,
hidden: false,
Expand All @@ -180,12 +184,13 @@ export default class Material {
this.densityProperty = new NumberProperty( options.density, {
// phetioFeatured: true,
// phetioDocumentation: 'Density of the object when the material is set to “CUSTOM”.',
range: new Range( 0, 23000 ),
range: options.densityRange,
units: 'kg/m^3'
} );
this.viscosity = options.viscosity;
this.custom = options.custom;
this.hidden = options.hidden;

this.customColor = options.customColor;
this.liquidColor = options.liquidColor;
this.depthLinesColorProperty = options.depthLinesColorProperty;
Expand Down Expand Up @@ -213,52 +218,35 @@ export default class Material {

/**
* Returns a custom material that can be modified at will, but with a liquid color specified.
*
* TODO: Delete once we better understand custom vs liquid colors, https://github.com/phetsims/density-buoyancy-common/issues/256
*/
public static createCustomLiquidMaterial( options: WithRequired<CreateCustomMaterialOptions, 'densityRange'> ): Material {
return Material.createCustomMaterial( combineOptions<MaterialOptions>( {

// TODO: Make sure to change the liquidColor when the density changes https://github.com/phetsims/density-buoyancy-common/issues/256
// This can be done by moving more things into the Material constructor, so they can use the densityProperty
liquidColor: Material.getCustomLiquidColor( options.density, options.densityRange )
public static createCustomLiquidMaterial( options: NoIdentifierMaterialOptions ): Material {
return new LiquidMaterial( combineOptions<MaterialOptions>( {
nameProperty: DensityBuoyancyCommonStrings.material.customStringProperty,
tandemName: 'custom',
identifier: 'CUSTOM',
custom: true
}, options ) );
}

/**
* Returns a custom material that can be modified at will, but with a solid color specified
*/
public static createCustomSolidMaterial( options: CreateCustomMaterialOptions ): Material {

assert && assert( options.hasOwnProperty( 'customColor' ) || options.hasOwnProperty( 'densityRange' ), 'we need a way to have a material color' );

// TODO: Make sure to change the solidColorProperty when the density changes https://github.com/phetsims/density-buoyancy-common/issues/256
const solidColorProperty = options.customColor || Material.getCustomSolidColor( options.density, options.densityRange! );

// TODO: Make sure to change the depthLinesColorPropertyProperty when the density changes https://github.com/phetsims/density-buoyancy-common/issues/256
const depthLinesColorPropertyProperty = new MappedProperty( solidColorProperty, {
map: solidColor => {

// The lighter depth line color has better contrast, so use that for more than half
const isDark = ( solidColor.r + solidColor.g + solidColor.b ) / 3 < 255 * 0.6;

return isDark ? DensityBuoyancyCommonColors.depthLinesLightColorProperty.value : DensityBuoyancyCommonColors.depthLinesDarkColorProperty.value;
}
} );

return Material.createCustomMaterial( combineOptions<MaterialOptions>( {
customColor: solidColorProperty,

// Also provide as a liquid color because some solid colors use Material.linkLiquidColor() (like the Bottle)
liquidColor: solidColorProperty,

depthLinesColorProperty: depthLinesColorPropertyProperty
public static createCustomSolidMaterial( options: NoIdentifierMaterialOptions ): Material {
return new SolidMaterial( combineOptions<MaterialOptions>( {
nameProperty: DensityBuoyancyCommonStrings.material.customStringProperty,
tandemName: 'custom',
identifier: 'CUSTOM',
custom: true
}, options ) );
}

/**
* Returns a value suitable for use in colors (0-255 value) that should be used as a grayscale value for
* a material of a given density. The mappíng is inverted, i.e. larger densities yield darker colors.
*/
private static getCustomLightness( density: number, densityRange: Range ): number {
protected static getCustomLightness( density: number, densityRange: Range ): number {
return Utils.roundSymmetric( this.getNormalizedLightness( density, densityRange ) * 255 );
}

Expand All @@ -276,24 +264,12 @@ export default class Material {
/**
* Similar to getCustomLightness, but returns the generated color, with an included alpha effect.
*/
public static getCustomLiquidColor( density: number, densityRange: Range ): ColorProperty {
public static getCustomLiquidColor( density: number, densityRange: Range ): Color {
const lightnessFactor = Material.getNormalizedLightness( density, densityRange );

return new ColorProperty(
Color.interpolateRGBA(
DensityBuoyancyCommonColors.customFluidDarkColorProperty.value,
DensityBuoyancyCommonColors.customFluidLightColorProperty.value,
lightnessFactor
) );
}

/**
* Similar to getCustomLightness, but returns the generated color
*/
private static getCustomSolidColor( density: number, densityRange: Range ): ColorProperty {
const lightness = Material.getCustomLightness( density, densityRange );

return new ColorProperty( new Color( lightness, lightness, lightness ) );
return Color.interpolateRGBA(
DensityBuoyancyCommonColors.customFluidDarkColorProperty.value,
DensityBuoyancyCommonColors.customFluidLightColorProperty.value,
lightnessFactor );
}

/**
Expand Down Expand Up @@ -805,9 +781,9 @@ export default class Material {
viscosity: material.viscosity,
custom: material.custom,
hidden: material.hidden,
staticCustomColor: NullableIO( Color.ColorIO ).toStateObject( isCustomColorUninstrumented ? material.customColor.value : null ),
staticCustomColor: NullableIO( Color.ColorIO ).toStateObject( isCustomColorUninstrumented ? material.customColor!.value : null ),
customColor: NullableColorPropertyReferenceType.toStateObject( isCustomColorUninstrumented ? null : material.customColor ),
staticLiquidColor: NullableIO( Color.ColorIO ).toStateObject( isLiquidColorUninstrumented ? material.liquidColor.value : null ),
staticLiquidColor: NullableIO( Color.ColorIO ).toStateObject( isLiquidColorUninstrumented ? material.liquidColor!.value : null ),
liquidColor: NullableColorPropertyReferenceType.toStateObject( isLiquidColorUninstrumented ? null : material.liquidColor ),
depthLinesColor: Color.ColorIO.toStateObject( material.depthLinesColorProperty.value )
};
Expand Down Expand Up @@ -837,6 +813,50 @@ export default class Material {
} );
}

class SolidMaterial extends Material {
public constructor( providedOptions: MaterialOptions ) {

const options = optionize<MaterialOptions, EmptySelfOptions, MaterialOptions>()( {}, providedOptions );

super( options );

if ( !this.customColor ) {
// TODO: can we make this field readonly again? https://github.com/phetsims/density-buoyancy-common/issues/256
this.customColor = new DerivedProperty( [ this.densityProperty, this.densityProperty.rangeProperty ], ( density, densityRange ) => {
const lightness = Material.getCustomLightness( density, densityRange );
return new Color( lightness, lightness, lightness );
} );
this.liquidColor = this.customColor;
}

this.depthLinesColorProperty = new MappedProperty( this.customColor, {
map: solidColor => {

// The lighter depth line color has better contrast, so use that for more than half
const isDark = ( solidColor.r + solidColor.g + solidColor.b ) / 3 < 255 * 0.6;

return isDark ? DensityBuoyancyCommonColors.depthLinesLightColorProperty.value : DensityBuoyancyCommonColors.depthLinesDarkColorProperty.value;
}
} );
}
}

class LiquidMaterial extends Material {
public constructor( providedOptions: MaterialOptions ) {

const options = optionize<MaterialOptions, EmptySelfOptions, MaterialOptions>()( {}, providedOptions );

super( options );
// TODO: This could be custom color given a "liquid" flag/subtype, https://github.com/phetsims/density-buoyancy-common/issues/256
if ( !this.liquidColor && this.custom ) {
// TODO: can we make this field readonly again? https://github.com/phetsims/density-buoyancy-common/issues/256
this.liquidColor = new DerivedProperty( [ this.densityProperty, this.densityProperty.rangeProperty ], ( density, densityRange ) => {
return Material.getCustomLiquidColor( density, densityRange );
} );
}
}
}

assert && assert( _.every( Material.MATERIALS, material => !( material.custom || material.identifier === 'CUSTOM' ) ),
'custom materials not allowed in MATERIALS list' );
assert && assert( _.uniq( Material.MATERIALS ).length === Material.MATERIALS.length, 'duplicate in Material.MATERIALS' );
Expand Down
20 changes: 15 additions & 5 deletions js/common/view/MaterialControlNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@

import Property from '../../../../axon/js/Property.js';
import optionize from '../../../../phet-core/js/optionize.js';
import { Color, ColorProperty, HBox, Node, Text, VBox, VBoxOptions } from '../../../../scenery/js/imports.js';
import { HBox, Node, Text, VBox, VBoxOptions } from '../../../../scenery/js/imports.js';
import ComboBox from '../../../../sun/js/ComboBox.js';
import densityBuoyancyCommon from '../../densityBuoyancyCommon.js';
import DensityBuoyancyCommonStrings from '../../DensityBuoyancyCommonStrings.js';
import DensityBuoyancyCommonConstants from '../DensityBuoyancyCommonConstants.js';
import Material, { MaterialName } from '../model/Material.js';
import { PhetioObjectOptions } from '../../../../tandem/js/PhetioObject.js';
import PickRequired from '../../../../phet-core/js/types/PickRequired.js';
import Range from '../../../../dot/js/Range.js';
import Utils from '../../../../dot/js/Utils.js';
import densityBuoyancyCommon from '../../densityBuoyancyCommon.js';

type SelfMaterialControlNodeOptions = {

Expand Down Expand Up @@ -96,9 +97,18 @@ export default class MaterialControlNode extends VBox {
};
};

// TODO: Yar, https://github.com/phetsims/density-buoyancy-common/issues/256
const customMaterial = Material.createCustomMaterial( {
customColor: new ColorProperty( new Color( 'green' ) )
// TODO: Yar? https://github.com/phetsims/density-buoyancy-common/issues/256
const customMaterial = Material.createCustomSolidMaterial( {
densityRange: this.customDensityRange,
density: materialProperty.value.density
} );
volumeProperty.link( volume => {
if ( materialProperty.value.custom ) {

// Handle our minimum volume if we're switched to custom (if needed)
const maxVolume = Math.max( volume, options.minCustomVolumeLiters / DensityBuoyancyCommonConstants.LITERS_IN_CUBIC_METER );
customMaterial.densityProperty.value = Utils.clamp( materialProperty.value.density, options.minCustomMass / maxVolume, options.maxCustomMass / maxVolume );
}
} );

// TODO: But can we just use the validValues of the provided MaterialProperty, https://github.com/phetsims/density-buoyancy-common/issues/256
Expand Down

0 comments on commit 64ca76f

Please sign in to comment.