diff --git a/js/coins/view/CoinExperimentMeasurementArea.ts b/js/coins/view/CoinExperimentMeasurementArea.ts index 968e0b4..6afc7da 100644 --- a/js/coins/view/CoinExperimentMeasurementArea.ts +++ b/js/coins/view/CoinExperimentMeasurementArea.ts @@ -10,7 +10,6 @@ import BooleanProperty from '../../../../axon/js/BooleanProperty.js'; import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; -import Multilink from '../../../../axon/js/Multilink.js'; import TProperty from '../../../../axon/js/TProperty.js'; import Bounds2 from '../../../../dot/js/Bounds2.js'; import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; @@ -142,14 +141,19 @@ export default class CoinExperimentMeasurementArea extends VBox { visibleProperty: sceneModel.preparingExperimentProperty } ); - const measuredCoinsPixelRepresentation = new CoinSetPixelRepresentation( sceneModel.systemType ); + const measuredCoinsPixelRepresentation = new CoinSetPixelRepresentation( + sceneModel.systemType, + sceneModel.coinSet.measurementStateProperty, + { + visibleProperty: new DerivedProperty( [ sceneModel.coinSet.numberOfActiveSystemsProperty ], numberOfActiveSystems => + numberOfActiveSystems === MAX_COINS ) + } ); const multipleCoinTestBoxContainer = new Node( { children: [ multipleCoinTestBox, measuredCoinsPixelRepresentation - ], - localBounds: multipleCoinTestBox.getGlobalBounds() + ] } ); // Create the composite node that represents to test box and controls where the user will experiment with multiple @@ -193,21 +197,11 @@ export default class CoinExperimentMeasurementArea extends VBox { this.measuredCoinsPixelRepresentation.setX( offset ); this.measuredCoinsPixelRepresentation.setY( offset ); - Multilink.multilink( - [ - sceneModel.coinSet.numberOfActiveSystemsProperty, - sceneModel.coinSet.measurementStateProperty - ], - ( numberOfActiveSystems, state ) => { - if ( numberOfActiveSystems === MAX_COINS && state === 'measuredAndRevealed' ) { - this.measuredCoinsPixelRepresentation.redraw( sceneModel.coinSet.measuredValues ); - this.measuredCoinsPixelRepresentation.visible = true; - } - else { - this.measuredCoinsPixelRepresentation.visible = false; - } + sceneModel.coinSet.measurementStateProperty.link( measurementState => { + if ( measurementState === 'measuredAndRevealed' ) { + this.measuredCoinsPixelRepresentation.redraw( sceneModel.coinSet.measuredValues ); } - ); + } ); // Create the node that will be used to cover (aka "mask") the coin so that its state can't be seen. const maskRadius = InitialCoinStateSelectorNode.INDICATOR_COIN_NODE_RADIUS * 1.02; @@ -258,6 +252,8 @@ export default class CoinExperimentMeasurementArea extends VBox { // experiments moving from the preparation area to the measurement area. singleCoinAnimations.startIngressAnimationForSingleCoin( false ); multipleCoinAnimations.startIngressAnimationForCoinSet( false ); + + this.measuredCoinsPixelRepresentation.startPopulatingAnimation(); } } ); @@ -267,15 +263,19 @@ export default class CoinExperimentMeasurementArea extends VBox { sceneModel.coinSet.measurementStateProperty.link( measurementState => { - if ( measurementState === 'preparingToBeMeasured' && sceneModel.systemType === 'quantum' ) { - - // Abort any previous animations and clear out the test box. - multipleCoinAnimations.abortIngressAnimationForCoinSet(); - multipleCoinTestBox.clearContents(); + if ( measurementState === 'preparingToBeMeasured' ) { + if ( sceneModel.systemType === 'classical' && sceneModel.coinSet.numberOfActiveSystemsProperty.value === 10000 ) { + this.measuredCoinsPixelRepresentation.startFlippingAnimation(); + } + else { + // Abort any previous animations and clear out the test box. + multipleCoinAnimations.abortIngressAnimationForCoinSet(); + multipleCoinTestBox.clearContents(); - // Animate a coin from the prep area to the single coin test box to indicate that a new "quantum coin" is - // being prepared for measurement. - multipleCoinAnimations.startIngressAnimationForCoinSet( true ); + // Animate a coin from the prep area to the single coin test box to indicate that a new "quantum coin" is + // being prepared for measurement. + multipleCoinAnimations.startIngressAnimationForCoinSet( true ); + } } } ); } diff --git a/js/coins/view/CoinSetPixelRepresentation.ts b/js/coins/view/CoinSetPixelRepresentation.ts index 981a828..99a4663 100644 --- a/js/coins/view/CoinSetPixelRepresentation.ts +++ b/js/coins/view/CoinSetPixelRepresentation.ts @@ -8,16 +8,95 @@ * */ +import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js'; +import dotRandom from '../../../../dot/js/dotRandom.js'; import { CanvasNode, CanvasNodeOptions } from '../../../../scenery/js/imports.js'; +import Animation from '../../../../twixt/js/Animation.js'; +import { MEASUREMENT_PREPARATION_TIME } from '../../common/model/TwoStateSystemSet.js'; import quantumMeasurement from '../../quantumMeasurement.js'; +import { ExperimentMeasurementState } from '../model/ExperimentMeasurementState.js'; export default class CoinSetPixelRepresentation extends CanvasNode { private readonly sideLength = 100; - private pixels: number[] = []; + private pixels = new Array( 100 * 100 ).fill( 0 ); private pixelScale = 1; - public constructor( private readonly systemType: 'classical' | 'quantum', providedOptions?: CanvasNodeOptions ) { + public readonly populatingAnimation: Animation; + public readonly flippingAnimation: Animation; + + public constructor( + private readonly systemType: 'classical' | 'quantum', + private readonly experimentStateProperty: TReadOnlyProperty, + providedOptions?: CanvasNodeOptions + ) { super( providedOptions ); + + const center = Math.floor( this.sideLength / 2 ); + const maxRadius = Math.sqrt( 2 ) * center * 1.1; // 10% extra radius to avoid missed pixels at the corners + const fps = 40; + const totalFrames = 4 * MEASUREMENT_PREPARATION_TIME / fps; + let currentFrame = 0; + + this.populatingAnimation = new Animation( { + to: totalFrames, + duration: MEASUREMENT_PREPARATION_TIME, + getValue: () => currentFrame, + setValue: frame => { + { + const progress = frame / totalFrames; + const currentRadius = progress * maxRadius; + + for ( let i = 0; i < this.sideLength; i++ ) { + for ( let j = 0; j < this.sideLength; j++ ) { + const dx = i - center; + const dy = j - center; + const distance = Math.sqrt( dx * dx + dy * dy ); + + if ( distance <= currentRadius ) { + const index = i * this.sideLength + j; + if ( this.pixels[ index ] === 0 ) { + // Some chance to populate a pixel + if ( dotRandom.nextDouble() < 0.3 ) { + this.pixels[ index ] = 1; // Set to grey + } + } + } + } + } + + this.invalidatePaint(); + currentFrame = frame; + } + } + } ); + + this.flippingAnimation = new Animation( { + to: 100, + duration: MEASUREMENT_PREPARATION_TIME, + getValue: () => currentFrame, + setValue: frame => { + for ( let i = 0; i < this.sideLength; i++ ) { + for ( let j = 0; j < this.sideLength; j++ ) { + const index = i * this.sideLength + j; + // Set a random value between 0 and 1 for each pixel + this.pixels[ index ] = dotRandom.nextInt( 2 ); + } + } + this.invalidatePaint(); + currentFrame = frame; + } + } ); + + this.populatingAnimation.finishEmitter.addListener( () => { + this.pixels = new Array( this.sideLength * this.sideLength ).fill( 1 ); + currentFrame = 0; + } ); + + this.flippingAnimation.finishEmitter.addListener( () => { + this.pixels = new Array( this.sideLength * this.sideLength ).fill( 1 ); + currentFrame = 0; + } ); + } public redraw( measuredValues: Array ): void { @@ -41,18 +120,53 @@ export default class CoinSetPixelRepresentation extends CanvasNode { */ public paintCanvas( context: CanvasRenderingContext2D ): void { + let getColor: ( value: number ) => string; + switch( this.experimentStateProperty.value ) { + case 'preparingToBeMeasured': + getColor = ( value: number ) => { + return value === 1 ? 'grey' : 'transparent'; + }; + break; + case 'measuredAndRevealed': + getColor = ( value: number ) => { + return value === 1 ? 'black' : 'fuchsia'; + }; + break; + case 'readyToBeMeasured': + getColor = ( value: number ) => { + return value === 1 ? 'grey' : 'transparent'; + }; + break; + default: + getColor = () => { + return 'transparent'; + }; + break; + } + context.save(); // Draw pixels on canvas for ( let i = 0; i < this.sideLength; i++ ) { for ( let j = 0; j < this.sideLength; j++ ) { const index = i * this.sideLength + j; - context.fillStyle = this.pixels[ index ] === 1 ? 'black' : 'fuchsia'; + context.fillStyle = getColor( this.pixels[ index ] ); context.fillRect( j * this.pixelScale, i * this.pixelScale, this.pixelScale, this.pixelScale ); } } context.restore(); } + + public startPopulatingAnimation(): void { + // Set all pixels to 0 + this.pixels = new Array( this.sideLength * this.sideLength ).fill( 0 ); + + this.populatingAnimation.start(); + } + + public startFlippingAnimation(): void { + this.flippingAnimation.start(); + } } quantumMeasurement.register( 'CoinSetPixelRepresentation', CoinSetPixelRepresentation ); \ No newline at end of file diff --git a/js/coins/view/MultiCoinTestBox.ts b/js/coins/view/MultiCoinTestBox.ts index f51e1d9..1f4af77 100644 --- a/js/coins/view/MultiCoinTestBox.ts +++ b/js/coins/view/MultiCoinTestBox.ts @@ -14,18 +14,18 @@ import Dimension2 from '../../../../dot/js/Dimension2.js'; import Vector2 from '../../../../dot/js/Vector2.js'; import { Shape } from '../../../../kite/js/imports.js'; import optionize, { EmptySelfOptions } from '../../../../phet-core/js/optionize.js'; -import PickRequired from '../../../../phet-core/js/types/PickRequired.js'; +import WithRequired from '../../../../phet-core/js/types/WithRequired.js'; import { Color, HBox, HBoxOptions, LinearGradient, Node, Rectangle } from '../../../../scenery/js/imports.js'; +import isSettingPhetioStateProperty from '../../../../tandem/js/isSettingPhetioStateProperty.js'; import TwoStateSystemSet from '../../common/model/TwoStateSystemSet.js'; import QuantumMeasurementConstants from '../../common/QuantumMeasurementConstants.js'; import quantumMeasurement from '../../quantumMeasurement.js'; +import { MAX_COINS, MULTI_COIN_EXPERIMENT_QUANTITIES } from '../model/CoinsExperimentSceneModel.js'; import { ExperimentMeasurementState } from '../model/ExperimentMeasurementState.js'; import SmallCoinNode from './SmallCoinNode.js'; -import { MAX_COINS, MULTI_COIN_EXPERIMENT_QUANTITIES } from '../model/CoinsExperimentSceneModel.js'; -import isSettingPhetioStateProperty from '../../../../tandem/js/isSettingPhetioStateProperty.js'; type SelfOptions = EmptySelfOptions; -export type MultiCoinTestBoxOptions = SelfOptions & PickRequired; +export type MultiCoinTestBoxOptions = SelfOptions & WithRequired; // constants const BOX_SIZE = new Dimension2( 200, 200 ); diff --git a/js/coins/view/MultipleCoinAnimations.ts b/js/coins/view/MultipleCoinAnimations.ts index 8eea99a..691432b 100644 --- a/js/coins/view/MultipleCoinAnimations.ts +++ b/js/coins/view/MultipleCoinAnimations.ts @@ -9,6 +9,7 @@ */ import TProperty from '../../../../axon/js/TProperty.js'; +import Tandem from '../../../../tandem/js/Tandem.js'; import Animation from '../../../../twixt/js/Animation.js'; import Easing from '../../../../twixt/js/Easing.js'; import { MEASUREMENT_PREPARATION_TIME } from '../../common/model/TwoStateSystemSet.js'; @@ -43,7 +44,7 @@ public readonly startIngressAnimationForCoinSet: ( forReprepare: boolean ) => vo const radius = MultiCoinTestBox.getRadiusFromCoinQuantity( quantity ); const coinNodes: SmallCoinNode[] = []; _.times( quantityToCreate, () => { - coinNodes.push( new SmallCoinNode( radius ) ); + coinNodes.push( new SmallCoinNode( radius, { visible: quantity !== 10000, tandem: Tandem.OPT_OUT } ) ); // TODO: Find a better way to not display these coins https://github.com/phetsims/quantum-measurement/issues/39 } ); movingCoinNodes.set( quantity, coinNodes ); } ); diff --git a/js/coins/view/SmallCoinNode.ts b/js/coins/view/SmallCoinNode.ts index 20cfccb..f639734 100644 --- a/js/coins/view/SmallCoinNode.ts +++ b/js/coins/view/SmallCoinNode.ts @@ -13,7 +13,7 @@ import TProperty from '../../../../axon/js/TProperty.js'; import dotRandom from '../../../../dot/js/dotRandom.js'; import Vector2 from '../../../../dot/js/Vector2.js'; import optionize, { EmptySelfOptions } from '../../../../phet-core/js/optionize.js'; -import PickRequired from '../../../../phet-core/js/types/PickRequired.js'; +import WithRequired from '../../../../phet-core/js/types/WithRequired.js'; import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; import { Circle, Color, Node, NodeOptions, Text } from '../../../../scenery/js/imports.js'; import Animation from '../../../../twixt/js/Animation.js'; @@ -23,7 +23,7 @@ import QuantumMeasurementConstants from '../../common/QuantumMeasurementConstant import quantumMeasurement from '../../quantumMeasurement.js'; type SelfOptions = EmptySelfOptions; -export type SmallCoinNodeOptions = SelfOptions & PickRequired; +export type SmallCoinNodeOptions = SelfOptions & WithRequired; export type SmallCoinDisplayMode = 'masked' | 'heads' | 'tails' | 'up' | 'down';