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 };