From a9243400897d98b5dbeba28028a974b5cf5231ca Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum <eric.rosenbaum@gmail.com> Date: Wed, 26 Sep 2018 17:59:30 -0400 Subject: [PATCH] Add an indicator to show that the microphone is listening (#3205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add mic indicator * Fix mic indicator position and margin * Don’t always show indicator, don’t sneaky-enable extension * Position mic indicator in RTL * Actually, don’t move indicator in RTL * Update event name for MIC_LISTENING * Move mic indicator state into redux * Move stageSizeToTransform into screen-utils * Position mic indicator and question at bottom of stage * Fix pointer events * JSDOC for stageSizeToTransform * Pass micIndicator into StageComponent via …props --- .../mic-indicator/mic-indicator.css | 10 ++++++ .../mic-indicator/mic-indicator.jsx | 29 ++++++++++++++++++ .../mic-indicator/mic-indicator.svg | Bin 0 -> 3029 bytes src/components/monitor-list/monitor-list.jsx | 12 +------- src/components/question/question.css | 7 ----- src/components/question/question.jsx | 4 ++- src/components/stage/stage.css | 27 +++++++++++++--- src/components/stage/stage.jsx | 29 ++++++++++++------ src/containers/stage.jsx | 4 ++- src/css/z-index.css | 1 + src/lib/screen-utils.js | 24 ++++++++++++++- src/lib/vm-listener-hoc.jsx | 8 +++++ src/reducers/gui.js | 3 ++ src/reducers/mic-indicator.js | 26 ++++++++++++++++ 14 files changed, 149 insertions(+), 35 deletions(-) create mode 100644 src/components/mic-indicator/mic-indicator.css create mode 100644 src/components/mic-indicator/mic-indicator.jsx create mode 100644 src/components/mic-indicator/mic-indicator.svg create mode 100644 src/reducers/mic-indicator.js diff --git a/src/components/mic-indicator/mic-indicator.css b/src/components/mic-indicator/mic-indicator.css new file mode 100644 index 000000000..9fb397029 --- /dev/null +++ b/src/components/mic-indicator/mic-indicator.css @@ -0,0 +1,10 @@ +@keyframes popIn { + from {transform: scale(0.5)} + to {transform: scale(1)} +} + +.mic-img { + margin: 10px; + transform-origin: center; + animation: popIn 0.1s ease-in-out; +} diff --git a/src/components/mic-indicator/mic-indicator.jsx b/src/components/mic-indicator/mic-indicator.jsx new file mode 100644 index 000000000..fe0eb602b --- /dev/null +++ b/src/components/mic-indicator/mic-indicator.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './mic-indicator.css'; +import micIcon from './mic-indicator.svg'; +import {stageSizeToTransform} from '../../lib/screen-utils'; + +const MicIndicatorComponent = props => ( + <div + className={props.className} + style={stageSizeToTransform(props.stageSize)} + > + <img + className={styles.micImg} + src={micIcon} + /> + </div> +); + +MicIndicatorComponent.propTypes = { + className: PropTypes.string, + stageSize: PropTypes.shape({ + width: PropTypes.number, + height: PropTypes.number, + widthDefault: PropTypes.number, + heightDefault: PropTypes.number + }).isRequired +}; + +export default MicIndicatorComponent; diff --git a/src/components/mic-indicator/mic-indicator.svg b/src/components/mic-indicator/mic-indicator.svg new file mode 100644 index 0000000000000000000000000000000000000000..78726389d13b811a8b7f31bbfa32a98df118a932 GIT binary patch literal 3029 zcma)8$!^<75Is9z(V;Ow0+_Ad7e{gy1<oZw4oNNva-gM_#IZ~oBxTtM=HKTv7m4F| zfXIf}e5`&|^{T3=>FfKOEx8L-otMQd;glsL6pM117wcK_?dxUglGhi{ruE&L?DJ)_ znI%@--X~-e^7W<}?(Xuie^=hm5=I!Y0{_XFlqUpkih7o8n&$R=GTHC<bT4UHttWyp zHUWKaa(=(fi|eNhx%Yn3K20v35qzf4)0BJ&MX0i-tj@`o>(DGVL~|<13vIPhFG)&9 z9<RzxxXH6VJ-R4wCUv(bO?%2FZ?@s$CSRm^vCJ2dcsl98&gU}Ji;H;`vL-B{NVDnP z(&?mq)LGTd!#0rS;Wo^YCfqlZMP2t%pG=?sn7@Df_3e+=^yE#t4_DWDlU|31Rh8X@ zn%wTTHTcP|&f<jq71RAinkp;mRaxDflX{VD!wW|J%UA#OR-8q<C#?9vrZ**T{@)jA zojt)a@)e_LKds9rY>ytv;PSNGHK7uRoQ}z)WtFe<;+#lxd*6P&>N6sPG+B|~WDPp> z{Gbh^x+Wp30}7Rbo8-kRFLJ1sZQ-bZeg~S-_LGxA+&?E6a`^ePZ<0w{-u`A8R&}?G zl1!_xKrG8ya+@`qG&Vk_3e)h=UHTbP?l?hlUBODI<YGFB`$u%EJZ2L~QlTmPt0fwi zJsH=meKL&XaNq6y22n70<<;L{epl|$*2NmLnzT`NHmT^FvnwmoScT84RUI0#j6<D} zCE~{=#~JaZ+*OP4c6+<Y5O}C}TO0%RCR>*KZqMgfX&#dqSdIy`8m+>IY**Kq#CO|W zMe1hxKHTMPfoF;8ygWhp$XVPwb~Wbw&EdS<mer@Msq%Yrmu+_elEkyB`y+=HLx2N{ zb-$#VAo*^b(TE@!2l+UVk*{MM5Th++hs48TXq)M}t;B!MoCKm&<uy)^qAbF2mktF_ z2*|d>!?;eX-4@p2E)?Z*IjZJ#kyi_xMX@Ej5$ran<!!den^;z=2fowGOJf{TU7+gH z;ELz*TD2Ecv|}5k^_U(V-^Medw|~aZV#oZ(--(kKIORuKwQaPkL$AZtHd|ajcHBjR zR-7ffYWw1}(?1n2F_ZBfbH1s<YL=Y#g9_&$JZARD#X*`U)czyB>12H*pGKzyCHW)` z_1;LS+?i6;8X*~1<l`82R5+)E<YYdEJ!Oo0r4`Ohs<`!>*)!#-c1#+tiPY3GC9!pk z&ZUAw=8WNE7^vbxDG|W~?-4x4@Ns0?1}vO5h>TIgw6{8@A-s25N+K+k!f3C|nc~!O zE0HO|L58Jb)|^T3YULf71Ju$ptN59;6hz)h)tV~96bEXklin()i7-?%$)vRr)rKps z!C)yjj7cRUDx72vN&@xV3FAd;>lteq9Od2^Cct2+P!LK(Ug4>-(mM+o7!^hcV+@gk zD(0m#kXImHqa+79lJ|(tCFJAMLW;35viI6DuOT{!l{S!tC`nOnZy;KXf?1SBkU3z@ zj8jatAuG?cFeJ)qq!+wRL|~<vh$tYh(LPZMuDOt4fS49FK<R;6&%D>30Cf(+GT1_5 z<xl||QNtxPYKy@P=PWWP%9&S8qAfWXl=WH?#O?&+&a}-cymid^*nUs6@OSd5mB>f4 z$nC7vI9<>xNSiZbC89gRl2h)GD~A{;weZ3jNyez6k_qR19FL<h3f%tIvkJ^37;j6k zp(n}6pO9#7W?fgkF0=zSkKM=_v_Cswv0BmyWDsA!J>dvEI{$b?F(E^w^AS4A>fmTB lKy0Yr)cW4Nr}ITwj85rGeByB#kCEYxg5zZxe@I_E`xi=f46^_L literal 0 HcmV?d00001 diff --git a/src/components/monitor-list/monitor-list.jsx b/src/components/monitor-list/monitor-list.jsx index 5cb114241..f7b02080f 100644 --- a/src/components/monitor-list/monitor-list.jsx +++ b/src/components/monitor-list/monitor-list.jsx @@ -4,20 +4,10 @@ import Box from '../box/box.jsx'; import Monitor from '../../containers/monitor.jsx'; import PropTypes from 'prop-types'; import {OrderedMap} from 'immutable'; +import {stageSizeToTransform} from '../../lib/screen-utils'; import styles from './monitor-list.css'; -const stageSizeToTransform = ({width, height, widthDefault, heightDefault}) => { - const scaleX = width / widthDefault; - const scaleY = height / heightDefault; - if (scaleX === 1 && scaleY === 1) { - // Do not set a transform if the scale is 1 because - // it messes up `position: fixed` elements like the context menu. - return; - } - return {transform: `scale(${scaleX},${scaleY})`}; -}; - const MonitorList = props => ( <Box // Use static `monitor-overlay` class for bounds of draggables diff --git a/src/components/question/question.css b/src/components/question/question.css index 4550df75c..a0f8116ed 100644 --- a/src/components/question/question.css +++ b/src/components/question/question.css @@ -1,13 +1,6 @@ @import "../../css/units.css"; @import "../../css/colors.css"; -.question-wrapper { - position: absolute; - bottom: 0; - left: 0; - width: 100%; -} - .question-container { margin: $space; border: 1px solid $ui-black-transparent; diff --git a/src/components/question/question.jsx b/src/components/question/question.jsx index c57fb085d..373f7cb91 100644 --- a/src/components/question/question.jsx +++ b/src/components/question/question.jsx @@ -7,13 +7,14 @@ import enterIcon from './icon--enter.svg'; const QuestionComponent = props => { const { answer, + className, question, onChange, onClick, onKeyPress } = props; return ( - <div className={styles.questionWrapper}> + <div className={className}> <div className={styles.questionContainer}> {question ? ( <div className={styles.questionLabel}>{question}</div> @@ -43,6 +44,7 @@ const QuestionComponent = props => { QuestionComponent.propTypes = { answer: PropTypes.string, + className: PropTypes.string, onChange: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, onKeyPress: PropTypes.func.isRequired, diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css index 7ce4d55d9..98a0c42a7 100644 --- a/src/components/stage/stage.css +++ b/src/components/stage/stage.css @@ -74,10 +74,6 @@ border: none; } -.question-wrapper { - position: absolute; -} - /* adjust monitors when stage is standard size: shift them down and right to compensate for the stage's border */ .stage-wrapper .monitor-wrapper { @@ -93,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, .queston-wrapper { +.monitor-wrapper, .color-picker-wrapper { position: absolute; top: 0; left: 0; @@ -110,3 +106,24 @@ to adjust for the border using a different method */ z-index: $z-index-dragging-sprite; filter: drop-shadow(5px 5px 5px $ui-black-transparent); } + +.stage-bottom-wrapper { + position: absolute; + display: flex; + flex-direction: column; + justify-content: flex-end; + top: 0; + overflow: hidden; + pointer-events: none; +} + +.mic-indicator { + transform-origin: bottom right; + z-index: $z-index-stage-indicator; + pointer-events: none; + align-self: flex-end; +} + +.question-wrapper { + pointer-events: auto; +} diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx index d8c0d0774..9e1f3454d 100644 --- a/src/components/stage/stage.jsx +++ b/src/components/stage/stage.jsx @@ -7,6 +7,7 @@ import DOMElementRenderer from '../../containers/dom-element-renderer.jsx'; import Loupe from '../loupe/loupe.jsx'; import MonitorList from '../../containers/monitor-list.jsx'; import Question from '../../containers/question.jsx'; +import MicIndicator from '../mic-indicator/mic-indicator.jsx'; import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js'; import {getStageDimensions} from '../../lib/screen-utils.js'; import styles from './stage.css'; @@ -18,6 +19,7 @@ const StageComponent = props => { isColorPicking, isFullScreen, colorInfo, + micIndicator, question, stageSize, useEditorDragStyle, @@ -66,13 +68,22 @@ const StageComponent = props => { <Loupe colorInfo={colorInfo} /> </Box> ) : null} - {question === null ? null : ( - <div - className={classNames( - styles.stageOverlayContent, - styles.stageOverlayContentBorderOverride - )} - > + <div + className={styles.stageBottomWrapper} + style={{ + width: stageDimensions.width, + height: stageDimensions.height, + left: '50%', + marginLeft: stageDimensions.width * -0.5 + }} + > + {micIndicator ? ( + <MicIndicator + className={styles.micIndicator} + stageSize={stageDimensions} + /> + ) : null} + {question === null ? null : ( <div className={styles.questionWrapper} style={{width: stageDimensions.width}} @@ -82,8 +93,8 @@ const StageComponent = props => { onQuestionAnswered={onQuestionAnswered} /> </div> - </div> - )} + )} + </div> <canvas className={styles.draggingSprite} height={0} diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index 0aa3f2a4b..dc2b87043 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -75,7 +75,8 @@ class Stage extends React.Component { this.props.isColorPicking !== nextProps.isColorPicking || this.state.colorInfo !== nextState.colorInfo || this.props.isFullScreen !== nextProps.isFullScreen || - this.state.question !== nextState.question; + this.state.question !== nextState.question || + this.props.micIndicator !== nextProps.micIndicator; } componentDidUpdate (prevProps) { if (this.props.isColorPicking && !prevProps.isColorPicking) { @@ -402,6 +403,7 @@ Stage.defaultProps = { const mapStateToProps = state => ({ isColorPicking: state.scratchGui.colorPicker.active, isFullScreen: state.scratchGui.mode.isFullScreen, + micIndicator: state.scratchGui.micIndicator, // Do not use editor drag style in fullscreen or player mode. useEditorDragStyle: !(state.scratchGui.mode.isFullScreen || state.scratchGui.mode.isPlayerOnly) }); diff --git a/src/css/z-index.css b/src/css/z-index.css index 87cc2cd01..06e201565 100644 --- a/src/css/z-index.css +++ b/src/css/z-index.css @@ -8,6 +8,7 @@ $z-index-extension-button: 50; /* Force extension button above the ScratchBlocks $z-index-menu-bar: 50; /* blocklyToolboxDiv is 40 */ $z-index-monitor: 100; +$z-index-stage-indicator: 110; $z-index-add-button: 120; $z-index-tooltip: 130; /* tooltips should go over add buttons if they overlap */ diff --git a/src/lib/screen-utils.js b/src/lib/screen-utils.js index afef90d84..d61201784 100644 --- a/src/lib/screen-utils.js +++ b/src/lib/screen-utils.js @@ -72,7 +72,29 @@ const getStageDimensions = (stageSize, isFullScreen) => { return stageDimensions; }; +/** + * Take a pair of sizes for the stage (a target height and width and a default height and width), + * calculate the ratio between them, and return a CSS transform to scale to that ratio. + * @param {object} sizeInfo An object containing dimensions of the target and default stage sizes. + * @param {number} sizeInfo.width The target width + * @param {number} sizeInfo.height The target height + * @param {number} sizeInfo.widthDefault The default width + * @param {number} sizeInfo.heightDefault The default height + * @returns {object} the CSS transform + */ +const stageSizeToTransform = ({width, height, widthDefault, heightDefault}) => { + const scaleX = width / widthDefault; + const scaleY = height / heightDefault; + if (scaleX === 1 && scaleY === 1) { + // Do not set a transform if the scale is 1 because + // it messes up `position: fixed` elements like the context menu. + return; + } + return {transform: `scale(${scaleX},${scaleY})`}; +}; + export { getStageDimensions, - resolveStageSize + resolveStageSize, + stageSizeToTransform }; diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index 68b2753ab..6b8d17777 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -10,6 +10,7 @@ import {updateBlockDrag} from '../reducers/block-drag'; import {updateMonitors} from '../reducers/monitors'; import {setRunningState, setTurboState} from '../reducers/vm-status'; import {showAlert} from '../reducers/alerts'; +import {updateMicIndicator} from '../reducers/mic-indicator'; /* * Higher Order Component to manage events emitted by the VM @@ -38,6 +39,8 @@ const vmListenerHOC = function (WrappedComponent) { this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart); this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop); this.props.vm.on('PERIPHERAL_ERROR', this.props.onShowAlert); + this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate); + } componentDidMount () { if (this.props.attachKeyboardEvents) { @@ -89,6 +92,7 @@ const vmListenerHOC = function (WrappedComponent) { onBlockDragUpdate, onKeyDown, onKeyUp, + onMicListeningUpdate, onMonitorsUpdate, onTargetsUpdate, onProjectRunStart, @@ -107,6 +111,7 @@ const vmListenerHOC = function (WrappedComponent) { onBlockDragUpdate: PropTypes.func.isRequired, onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, + onMicListeningUpdate: PropTypes.func.isRequired, onMonitorsUpdate: PropTypes.func.isRequired, onProjectRunStart: PropTypes.func.isRequired, onProjectRunStop: PropTypes.func.isRequired, @@ -141,6 +146,9 @@ const vmListenerHOC = function (WrappedComponent) { onTurboModeOff: () => dispatch(setTurboState(false)), onShowAlert: data => { dispatch(showAlert(data)); + }, + onMicListeningUpdate: listening => { + dispatch(updateMicIndicator(listening)); } }); return connect( diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 4d6cea9c0..bce112a1c 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -8,6 +8,7 @@ import blockDragReducer, {blockDragInitialState} from './block-drag'; import editorTabReducer, {editorTabInitialState} from './editor-tab'; import hoveredTargetReducer, {hoveredTargetInitialState} from './hovered-target'; import menuReducer, {menuInitialState} from './menus'; +import micIndicatorReducer, {micIndicatorInitialState} from './mic-indicator'; import modalReducer, {modalsInitialState} from './modals'; import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; @@ -38,6 +39,7 @@ const guiInitialState = { hoveredTarget: hoveredTargetInitialState, stageSize: stageSizeInitialState, menus: menuInitialState, + micIndicator: micIndicatorInitialState, modals: modalsInitialState, monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, @@ -104,6 +106,7 @@ const guiReducer = combineReducers({ hoveredTarget: hoveredTargetReducer, stageSize: stageSizeReducer, menus: menuReducer, + micIndicator: micIndicatorReducer, modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer, diff --git a/src/reducers/mic-indicator.js b/src/reducers/mic-indicator.js new file mode 100644 index 000000000..2548abbb4 --- /dev/null +++ b/src/reducers/mic-indicator.js @@ -0,0 +1,26 @@ +const UPDATE = 'scratch-gui/mic-indicator/UPDATE'; + +const initialState = false; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case UPDATE: + return action.visible; + default: + return state; + } +}; + +const updateMicIndicator = function (visible) { + return { + type: UPDATE, + visible: visible + }; +}; + +export { + reducer as default, + initialState as micIndicatorInitialState, + updateMicIndicator +}; -- GitLab