Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spritelab] Implement Pause button behind experiment #38457

Merged
merged 6 commits into from
Jan 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion apps/src/p5lab/P5Lab.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,10 @@ P5Lab.prototype.init = function(config) {
(!config.hideSource &&
!config.level.debuggerDisabled &&
!config.level.iframeEmbedAppAndCode);
var showPauseButton = experiments.isEnabled(experiments.SPRITELAB_PAUSE);
var showDebugConsole = config.level.editCode && !config.hideSource;
this.debuggerEnabled = showDebugButtons || showDebugConsole;
this.debuggerEnabled =
showDebugButtons || showPauseButton || showDebugConsole;

if (this.debuggerEnabled) {
getStore().dispatch(
Expand Down Expand Up @@ -492,6 +494,7 @@ P5Lab.prototype.init = function(config) {
<P5LabView
showFinishButton={finishButtonFirstLine && showFinishButton}
onMount={onMount}
pauseHandler={this.onPause}
/>
</Provider>,
document.getElementById(config.containerId)
Expand Down Expand Up @@ -647,6 +650,11 @@ P5Lab.prototype.setupReduxSubscribers = function(store) {
});
};

/**
* Override to change pause behavior.
*/
P5Lab.prototype.onPause = function() {};

P5Lab.prototype.onIsRunningChange = function() {
this.setCrosshairCursorForPlaySpace();
};
Expand Down
8 changes: 6 additions & 2 deletions apps/src/p5lab/P5LabView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class P5LabView extends React.Component {
isIframeEmbed: PropTypes.bool.isRequired,
isRunning: PropTypes.bool.isRequired,
spriteLab: PropTypes.bool.isRequired,
isBackground: PropTypes.bool
isBackground: PropTypes.bool,
pauseHandler: PropTypes.func.isRequired
};

state = {
Expand Down Expand Up @@ -119,7 +120,10 @@ class P5LabView extends React.Component {
style={visualizationColumnStyle}
>
{this.props.showVisualizationHeader && <P5LabVisualizationHeader />}
<P5LabVisualizationColumn finishButton={showFinishButton} />
<P5LabVisualizationColumn
finishButton={showFinishButton}
pauseHandler={this.props.pauseHandler}
/>
{this.getChannelId() && (
<AnimationPicker
channelId={this.getChannelId()}
Expand Down
21 changes: 17 additions & 4 deletions apps/src/p5lab/P5LabVisualizationColumn.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {connect} from 'react-redux';
import classNames from 'classnames';
import GameButtons from '@cdo/apps/templates/GameButtons';
import ArrowButtons from '@cdo/apps/templates/ArrowButtons';
import PauseButton from '@cdo/apps/templates/PauseButton';
import BelowVisualization from '@cdo/apps/templates/BelowVisualization';
import experiments from '@cdo/apps/util/experiments';
import {APP_HEIGHT, APP_WIDTH} from './constants';
Expand Down Expand Up @@ -56,9 +57,20 @@ class P5LabVisualizationColumn extends React.Component {
cancelPicker: PropTypes.func.isRequired,
selectPicker: PropTypes.func.isRequired,
updatePicker: PropTypes.func.isRequired,
consoleMessages: PropTypes.array.isRequired
consoleMessages: PropTypes.array.isRequired,
pauseHandler: PropTypes.func.isRequired
};

constructor(props) {
super(props);
this.spritelabPauseExperiment = experiments.isEnabled(
experiments.SPRITELAB_PAUSE
);
this.spritelabInputExperiment = experiments.isEnabled(
experiments.SPRITELAB_INPUT
);
}

// Cache app-space mouse coordinates, which we get from the
// VisualizationOverlay when they change.
state = {
Expand Down Expand Up @@ -186,12 +198,13 @@ class P5LabVisualizationColumn extends React.Component {
</VisualizationOverlay>
</ProtectedVisualizationDiv>
<TextConsole consoleMessages={this.props.consoleMessages} />
{experiments.isEnabled(experiments.SPRITELAB_INPUT) && (
<SpritelabInput />
)}
{this.spritelabInputExperiment && <SpritelabInput />}
</div>

<GameButtons>
{this.spritelabPauseExperiment && (
<PauseButton pauseHandler={this.props.pauseHandler} />
)}
<ArrowButtons />

<CompletionButton />
Expand Down
9 changes: 9 additions & 0 deletions apps/src/p5lab/spritelab/SpriteLab.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,12 @@ SpriteLab.prototype.reset = function() {
getStore().dispatch(clearPrompts());
this.preview();
};

SpriteLab.prototype.onPause = function(isPaused) {
const current = new Date().getTime();
if (isPaused) {
coreLibrary.endPause(current);
} else {
coreLibrary.startPause(current);
}
};
2 changes: 1 addition & 1 deletion apps/src/p5lab/spritelab/commands/worldCommands.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const commands = {

getTime(unit) {
if (unit === 'seconds') {
return this.World.seconds || 0;
return coreLibrary.getAdjustedWorldTime(this) || 0;
} else if (unit === 'frames') {
return this.World.frameCount || 0;
}
Expand Down
35 changes: 27 additions & 8 deletions apps/src/p5lab/spritelab/coreLibrary.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ var inputEvents = [];
var behaviors = [];
var userInputEventCallbacks = {};
var newSprites = {};
var totalPauseTime = 0;
var currentPauseStartTime = 0;

export var background;
export var title = '';
Expand All @@ -19,6 +21,24 @@ export function reset() {
title = subtitle = '';
userInputEventCallbacks = {};
defaultSpriteSize = 100;
totalPauseTime = 0;
}

export function startPause(time) {
currentPauseStartTime = time;
}

export function endPause(time) {
totalPauseTime += time - currentPauseStartTime;
currentPauseStartTime = 0;
}

/**
* Returns World.seconds adjusted to exclude time during which the app was paused
*/
export function getAdjustedWorldTime(p5Inst) {
const current = new Date().getTime();
return Math.round((current - p5Inst._startTime - totalPauseTime) / 1000);
}

/**
Expand Down Expand Up @@ -205,13 +225,11 @@ export function clearCollectDataEvents() {
function atTimeEvent(inputEvent, p5Inst) {
if (inputEvent.args.unit === 'seconds') {
const previousTime = inputEvent.previousTime || 0;
inputEvent.previousTime = p5Inst.World.seconds;
const worldTime = getAdjustedWorldTime(p5Inst);
inputEvent.previousTime = worldTime;
// There are many ticks per second, but we only want to fire the event once (on the first tick where
// World.seconds matches the event argument)
if (
p5Inst.World.seconds === inputEvent.args.n &&
previousTime !== inputEvent.args.n
) {
// the time matches the event argument)
if (worldTime === inputEvent.args.n && previousTime !== inputEvent.args.n) {
// Call callback with no extra args
return [{}];
}
Expand All @@ -227,10 +245,11 @@ function atTimeEvent(inputEvent, p5Inst) {

function collectDataEvent(inputEvent, p5Inst) {
const previous = inputEvent.previous || 0;
inputEvent.previous = p5Inst.World.seconds;
const worldTime = getAdjustedWorldTime(p5Inst);
inputEvent.previous = worldTime;

// Only log data once per second
if (p5Inst.World.seconds !== previous) {
if (worldTime !== previous) {
// Call callback with no extra args
return [{}];
} else {
Expand Down
57 changes: 57 additions & 0 deletions apps/src/templates/PauseButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import color from '@cdo/apps/util/color';
import {actions, selectors} from '@cdo/apps/lib/tools/jsdebugger/redux';

const styles = {
button: {
minWidth: 0,
padding: '10px 13px',
backgroundColor: color.orange,
borderColor: color.orange,
color: color.white
}
};

class PauseButton extends React.Component {
static propTypes = {
pauseHandler: PropTypes.func.isRequired,
// from redux
togglePause: PropTypes.func.isRequired,
isPaused: PropTypes.bool.isRequired,
isAttached: PropTypes.bool.isRequired
};

state = {
pauseStart: 0
};

togglePause = () => {
this.props.pauseHandler(this.props.isPaused);
this.props.togglePause();
};

render() {
return (
<button
type="button"
onClick={this.togglePause}
style={styles.button}
disabled={!this.props.isAttached}
>
<i className={this.props.isPaused ? 'fa fa-play' : 'fa fa-pause'} />
</button>
);
}
}

export default connect(
state => ({
isAttached: selectors.isAttached(state),
isPaused: selectors.isPaused(state)
}),
{
togglePause: actions.togglePause
}
)(PauseButton);
1 change: 1 addition & 0 deletions apps/src/util/experiments.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ experiments.SPRITELAB_INPUT = 'spritelabInput';
experiments.TIME_SPENT = 'time-spent';
experiments.APPLAB_ML = 'applab-ml';
experiments.BYPASS_DIALOG_POPUP = 'bypass-dialog-popup';
experiments.SPRITELAB_PAUSE = 'spritelabPause';

/**
* Get our query string. Provided as a method so that tests can mock this.
Expand Down