Skip to content

Commit

Permalink
Improving on the 10000 coins animations, see #39
Browse files Browse the repository at this point in the history
  • Loading branch information
AgustinVallejo committed Oct 1, 2024
1 parent 5afd852 commit 241cb19
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 36 deletions.
52 changes: 26 additions & 26 deletions js/coins/view/CoinExperimentMeasurementArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
} );

Expand All @@ -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 );
}
}
} );
}
Expand Down
120 changes: 117 additions & 3 deletions js/coins/view/CoinSetPixelRepresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExperimentMeasurementState>,
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<string | null> ): void {
Expand All @@ -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 );
8 changes: 4 additions & 4 deletions js/coins/view/MultiCoinTestBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HBoxOptions, 'tandem'>;
export type MultiCoinTestBoxOptions = SelfOptions & WithRequired<HBoxOptions, 'tandem'>;

// constants
const BOX_SIZE = new Dimension2( 200, 200 );
Expand Down
3 changes: 2 additions & 1 deletion js/coins/view/MultipleCoinAnimations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 );
} );
Expand Down
4 changes: 2 additions & 2 deletions js/coins/view/SmallCoinNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,7 +23,7 @@ import QuantumMeasurementConstants from '../../common/QuantumMeasurementConstant
import quantumMeasurement from '../../quantumMeasurement.js';

type SelfOptions = EmptySelfOptions;
export type SmallCoinNodeOptions = SelfOptions & PickRequired<NodeOptions, 'tandem'>;
export type SmallCoinNodeOptions = SelfOptions & WithRequired<NodeOptions, 'tandem'>;

export type SmallCoinDisplayMode = 'masked' | 'heads' | 'tails' | 'up' | 'down';

Expand Down

0 comments on commit 241cb19

Please sign in to comment.