diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css
index 98a0c42a7..9264c094e 100644
--- a/src/components/stage/stage.css
+++ b/src/components/stage/stage.css
@@ -89,7 +89,7 @@ to adjust for the border using a different method */
padding-bottom: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width);
}
-.monitor-wrapper, .color-picker-wrapper {
+.monitor-wrapper, .color-picker-wrapper, .frame-wrapper {
position: absolute;
top: 0;
left: 0;
@@ -127,3 +127,18 @@ to adjust for the border using a different method */
.question-wrapper {
pointer-events: auto;
}
+
+.frame {
+ box-sizing: content-box !important;
+ background: $motion-transparent;
+ border: 2px solid $motion-primary;
+ border-radius: 0.5rem;
+ animation-name: flash;
+ animation-duration: 0.75s;
+ animation-fill-mode: forwards; /* Leave at 0 opacity after animation */
+}
+
+@keyframes flash {
+ 0% { opacity: 1; }
+ 100% { opacity: 0; }
+}
diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx
index 2bf7334ac..1cf1aed7e 100644
--- a/src/components/stage/stage.jsx
+++ b/src/components/stage/stage.jsx
@@ -6,6 +6,7 @@ import Box from '../box/box.jsx';
import DOMElementRenderer from '../../containers/dom-element-renderer.jsx';
import Loupe from '../loupe/loupe.jsx';
import MonitorList from '../../containers/monitor-list.jsx';
+import TargetHighlight from '../../containers/target-highlight.jsx';
import Question from '../../containers/question.jsx';
import MicIndicator from '../mic-indicator/mic-indicator.jsx';
import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js';
@@ -63,6 +64,9 @@ const StageComponent = props => {
stageSize={stageDimensions}
/>
+
+
+
{isColorPicking && colorInfo ? (
diff --git a/src/containers/target-highlight.jsx b/src/containers/target-highlight.jsx
new file mode 100644
index 000000000..a1e90aafd
--- /dev/null
+++ b/src/containers/target-highlight.jsx
@@ -0,0 +1,78 @@
+import bindAll from 'lodash.bindall';
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import {connect} from 'react-redux';
+import VM from 'scratch-vm';
+
+class TargetHighlight extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'getPageCoords'
+ ]);
+ }
+
+ // Transform scratch coordinates into page coordinates
+ getPageCoords (x, y) {
+ const nativeSize = this.props.vm.renderer.getNativeSize();
+ const rect = this.props.vm.renderer.canvas.getBoundingClientRect();
+ return [
+ ((rect.width / nativeSize[0]) * x) + (rect.width / 2),
+ -((rect.height / nativeSize[1]) * y) + (rect.height / 2)
+ ];
+ }
+
+ render () {
+ const {
+ className,
+ highlightedTargetId,
+ highlightedTargetTime,
+ vm
+ } = this.props;
+
+ if (!(highlightedTargetId && vm && vm.renderer)) return null;
+
+ const target = vm.runtime.getTargetById(highlightedTargetId);
+ const bounds = vm.renderer.getBounds(target.drawableID);
+ const [left, top] = this.getPageCoords(bounds.left, bounds.top);
+ const [right, bottom] = this.getPageCoords(bounds.right, bounds.bottom);
+
+ const pad = 2; // px
+
+ return (
+
+ );
+ }
+}
+
+TargetHighlight.propTypes = {
+ className: PropTypes.string,
+ highlightedTargetId: PropTypes.string,
+ highlightedTargetTime: PropTypes.number,
+ vm: PropTypes.instanceOf(VM)
+};
+
+const mapStateToProps = state => ({
+ highlightedTargetTime: state.scratchGui.targets.highlightedTargetTime,
+ highlightedTargetId: state.scratchGui.targets.highlightedTargetId,
+ vm: state.scratchGui.vm
+});
+
+const mapDispatchToProps = () => ({});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TargetHighlight);
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index 3d54b86cb..c34d34133 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -18,6 +18,7 @@ import spriteLibraryContent from '../lib/libraries/sprites.json';
import {handleFileUpload, spriteUpload} from '../lib/file-uploader.js';
import sharedMessages from '../lib/shared-messages';
import {emptySprite} from '../lib/empty-assets';
+import {highlightTarget} from '../reducers/targets';
class TargetPane extends React.Component {
constructor (props) {
@@ -109,6 +110,7 @@ class TargetPane extends React.Component {
}
handleSelectSprite (id) {
this.props.vm.setEditingTarget(id);
+ this.props.onHighlightTarget(id, Date.now());
}
handleSurpriseSpriteClick () {
const item = spriteLibraryContent[Math.floor(Math.random() * spriteLibraryContent.length)];
@@ -195,6 +197,7 @@ class TargetPane extends React.Component {
const {
onActivateTab, // eslint-disable-line no-unused-vars
onReceivedBlocks, // eslint-disable-line no-unused-vars
+ onHighlightTarget, // eslint-disable-line no-unused-vars
dispatchUpdateRestore, // eslint-disable-line no-unused-vars
...componentProps
} = this.props;
@@ -265,6 +268,9 @@ const mapDispatchToProps = dispatch => ({
},
dispatchUpdateRestore: restoreState => {
dispatch(setRestore(restoreState));
+ },
+ onHighlightTarget: (id, time) => {
+ dispatch(highlightTarget(id, time));
}
});
diff --git a/src/reducers/targets.js b/src/reducers/targets.js
index c1f661394..58805bce6 100644
--- a/src/reducers/targets.js
+++ b/src/reducers/targets.js
@@ -1,8 +1,11 @@
const UPDATE_TARGET_LIST = 'scratch-gui/targets/UPDATE_TARGET_LIST';
+const HIGHLIGHT_TARGET = 'scratch-gui/targets/HIGHLIGHT_TARGET';
const initialState = {
sprites: {},
- stage: {}
+ stage: {},
+ highlightedTargetId: null,
+ highlightedTargetTime: null
};
const reducer = function (state, action) {
@@ -23,6 +26,11 @@ const reducer = function (state, action) {
.filter(target => target.isStage)[0] || {},
editingTarget: action.editingTarget
});
+ case HIGHLIGHT_TARGET:
+ return Object.assign({}, state, {
+ highlightedTargetId: action.targetId,
+ highlightedTargetTime: action.updateTime
+ });
default:
return state;
}
@@ -37,8 +45,16 @@ const updateTargets = function (targetList, editingTarget) {
}
};
};
+const highlightTarget = function (targetId, updateTime) {
+ return {
+ type: HIGHLIGHT_TARGET,
+ targetId: targetId,
+ updateTime: updateTime
+ };
+};
export {
reducer as default,
initialState as targetsInitialState,
- updateTargets
+ updateTargets,
+ highlightTarget
};