Merge remote-tracking branch 'upstream/develop' into add-full-screen-render-view

** Merging in loupe color picker changes from upstream. Resolved conflicts, which
     required some minor changes. The color picker menu from scratch-blocks along
     with other workspace menus will show through stage when zoomed if opened.

     Resolved by moving stage/stage-header z-index from 999 to 5000.
This commit is contained in:
Josiah Neuberger committed 2017-10-01 00:50:15 -04:00
commit e3221cecd9
9 files changed
+299 -40

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
.color-picker {
position: absolute;
border-radius: 100%;
border: 1px solid #222;
}
+104
View File
@@ -0,0 +1,104 @@
import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import Box from '../box/box.jsx';
import styles from './loupe.css';
const zoomScale = 3;
class LoupeComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'setCanvas'
]);
}
componentDidUpdate () {
this.draw();
}
draw () {
const boxSize = 6 / zoomScale;
const boxLineWidth = 1 / zoomScale;
const colorRingWidth = 15 / zoomScale;
const ctx = this.canvas.getContext('2d');
const {color, data, width, height} = this.props.colorInfo;
this.canvas.width = zoomScale * width;
this.canvas.height = zoomScale * height;
// In order to scale the image data, must draw to a tmp canvas first
const tmpCanvas = document.createElement('canvas');
tmpCanvas.width = width;
tmpCanvas.height = height;
const tmpCtx = tmpCanvas.getContext('2d');
const imageData = tmpCtx.createImageData(width, height);
imageData.data.set(data);
tmpCtx.putImageData(imageData, 0, 0);
// Scale the loupe canvas and draw the zoomed image
ctx.save();
ctx.scale(zoomScale, zoomScale);
ctx.drawImage(tmpCanvas, 0, 0, width, height);
// Draw an outlined square at the cursor position (cursor is hidden)
ctx.lineWidth = boxLineWidth;
ctx.strokeStyle = 'black';
ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
ctx.beginPath();
ctx.rect((width / 2) - (boxSize / 2), (height / 2) - (boxSize / 2), boxSize, boxSize);
ctx.fill();
ctx.stroke();
// Draw a thick ring around the loupe showing the current color
ctx.strokeStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
ctx.lineWidth = colorRingWidth;
ctx.beginPath();
ctx.moveTo(width, height / 2);
ctx.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI);
ctx.stroke();
ctx.restore();
}
setCanvas (element) {
this.canvas = element;
}
render () {
const {
colorInfo,
...boxProps
} = this.props;
return (
<Box
{...boxProps}
className={styles.colorPicker}
componentRef={this.setCanvas}
element="canvas"
height={colorInfo.height}
style={{
top: colorInfo.y - ((zoomScale * colorInfo.height) / 2),
left: colorInfo.x - ((zoomScale * colorInfo.width) / 2),
width: colorInfo.width * zoomScale,
height: colorInfo.height * zoomScale
}}
width={colorInfo.width}
/>
);
}
}
LoupeComponent.propTypes = {
colorInfo: PropTypes.shape({
color: PropTypes.shape({
r: PropTypes.number,
g: PropTypes.number,
b: PropTypes.number
}),
data: PropTypes.instanceOf(Uint8Array),
width: PropTypes.number,
height: PropTypes.number,
x: PropTypes.number,
y: PropTypes.number
})
};
export default LoupeComponent;
+1 -1
View File
@@ -11,7 +11,7 @@
top: 0;
left: 0;
right: 0;
z-index: 999;
z-index: 5000;
}
.stage-menu-wrapper {
+19 -2
View File
@@ -13,6 +13,22 @@
background-color: transparent;
}
.with-color-picker {
cursor: none;
z-index: 2001;
}
.color-picker-background {
position: absolute;;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.55);
display: block;
z-index: 2000;
top: 0;
left: 0;
}
.stage-wrapper {
position: relative;
}
@@ -23,7 +39,7 @@
left: 0;
right: 0;
bottom: 0;
z-index: 999;
z-index: 5000;
background-color: rgba(255, 255, 255, 1);
}
@@ -40,11 +56,12 @@
justify-content: center;
}
.monitor-wrapper {
.monitor-wrapper, .color-picker-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
}
+50 -24
View File
@@ -1,8 +1,9 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import Box from '../box/box.jsx';
import Loupe from '../loupe/loupe.jsx';
import MonitorList from '../../containers/monitor-list.jsx';
import styles from './stage.css';
@@ -10,8 +11,11 @@ const StageComponent = props => {
const {
canvasRef,
height,
isColorPicking,
isZoomed,
width,
colorInfo,
onDeactivateColorPicker,
...boxProps
} = props;
let heightCorrectedAspect = window.innerHeight - 40;
@@ -21,39 +25,61 @@ const StageComponent = props => {
heightCorrectedAspect = widthCorrectedAspect * .75;
}
return isZoomed === false ? (
<Box className={styles.stageWrapper}>
<div>
<Box
className={styles.stage}
componentRef={canvasRef}
element="canvas"
height={height}
width={width}
{...boxProps}
/>
<Box className={styles.monitorWrapper}>
<MonitorList />
className={classNames(styles.stageWrapper, {
[styles.withColorPicker]: isColorPicking
})}
>
<Box
className={styles.stage}
componentRef={canvasRef}
element="canvas"
height={height}
width={width}
{...boxProps}
/>
<Box className={styles.monitorWrapper}>
<MonitorList />
</Box>
{isColorPicking && colorInfo ? (
<Box className={styles.colorPickerWrapper}>
<Loupe colorInfo={colorInfo} />
</Box>
) : null}
</Box>
</Box>
{isColorPicking ? (
<Box
className={styles.colorPickerBackground}
onClick={onDeactivateColorPicker}
/>
) : null}
</div>
) : (
<Box className={styles.stageWrapperOverlay}>
<Box
className={classNames(styles.stage, styles.stageOverlayContent)}
componentRef={canvasRef}
element="canvas"
height={heightCorrectedAspect}
width={widthCorrectedAspect}
{...boxProps}
/>
<Box className={styles.monitorWrapper}>
<MonitorList />
<div>
<Box className={styles.stageWrapperOverlay}>
<Box
className={classNames(styles.stage, styles.stageOverlayContent)}
componentRef={canvasRef}
element="canvas"
height={heightCorrectedAspect}
width={widthCorrectedAspect}
{...boxProps}
/>
<Box className={styles.monitorWrapper}>
<MonitorList />
</Box>
</Box>
</Box>
</div>
);
};
StageComponent.propTypes = {
canvasRef: PropTypes.func,
colorInfo: Loupe.propTypes.colorInfo,
height: PropTypes.number,
isColorPicking: PropTypes.bool,
isZoomed: PropTypes.bool.isRequired,
onDeactivateColorPicker: PropTypes.func,
width: PropTypes.number
};
StageComponent.defaultProps = {
+13 -5
View File
@@ -11,6 +11,7 @@ import BlocksComponent from '../components/blocks/blocks.jsx';
import {connect} from 'react-redux';
import {updateToolbox} from '../reducers/toolbox';
import {activateColorPicker} from '../reducers/color-picker';
const addFunctionListener = (object, property, callback) => {
const oldFn = object[property];
@@ -50,6 +51,8 @@ class Blocks extends React.Component {
this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
}
componentDidMount () {
this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker;
const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
@@ -219,14 +222,17 @@ class Blocks extends React.Component {
this.setState({prompt: null});
}
render () {
/* eslint-disable no-unused-vars */
const {
options, // eslint-disable-line no-unused-vars
vm, // eslint-disable-line no-unused-vars
isVisible, // eslint-disable-line no-unused-vars
onExtensionAdded, // eslint-disable-line no-unused-vars
toolboxXML, // eslint-disable-line no-unused-vars
options,
vm,
isVisible,
onActivateColorPicker,
onExtensionAdded,
toolboxXML,
...props
} = this.props;
/* eslint-enable no-unused-vars */
return (
<div>
<BlocksComponent
@@ -249,6 +255,7 @@ class Blocks extends React.Component {
Blocks.propTypes = {
isVisible: PropTypes.bool,
onActivateColorPicker: PropTypes.func,
onExtensionAdded: PropTypes.func,
options: PropTypes.shape({
media: PropTypes.string,
@@ -311,6 +318,7 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
onActivateColorPicker: callback => dispatch(activateColorPicker(callback)),
onExtensionAdded: toolboxXML => {
dispatch(updateToolbox(toolboxXML));
}
+65 -7
View File
@@ -3,12 +3,19 @@ import PropTypes from 'prop-types';
import React from 'react';
import Renderer from 'scratch-render';
import VM from 'scratch-vm';
import {getEventXY} from '../lib/touch-utils';
import {connect} from 'react-redux';
import {getEventXY} from '../lib/touch-utils';
import StageComponent from '../components/stage/stage.jsx';
import {
activateColorPicker,
deactivateColorPicker
} from '../reducers/color-picker';
const colorPickerRadius = 20;
class Stage extends React.Component {
constructor (props) {
super(props);
@@ -29,9 +36,10 @@ class Stage extends React.Component {
mouseDownTimeoutId: null,
mouseDownPosition: null,
isDragging: false,
isZoomed: false,
dragOffset: null,
dragId: null,
isZoomed: false
colorInfo: null
};
}
componentDidMount () {
@@ -50,16 +58,32 @@ class Stage extends React.Component {
}
shouldComponentUpdate (nextProps, nextState) {
return this.props.width !== nextProps.width ||
this.props.height !== nextProps.height ||
this.state.isZoomed !== nextState.isZoomed;
this.props.height !== nextProps.height ||
this.props.isColorPicking !== nextProps.isColorPicking ||
this.state.colorInfo !== nextState.colorInfo ||
this.state.isZoomed !== nextState.isZoomed;
}
componentDidUpdate () {
componentDidUpdate (prevProps) {
if (this.props.isColorPicking && !prevProps.isColorPicking) {
this.startColorPickingLoop();
} else if (!this.props.isColorPicking && prevProps.isColorPicking) {
this.stopColorPickingLoop();
}
this.updateRect();
this.renderer.resize(this.rect.width, this.rect.height);
}
componentWillUnmount () {
this.detachMouseEvents(this.canvas);
this.detachRectEvents();
this.stopColorPickingLoop();
}
startColorPickingLoop () {
this.intervalId = setInterval(() => {
this.setState({colorInfo: this.getColorInfo(this.pickX, this.pickY)});
}, 30);
}
stopColorPickingLoop () {
clearInterval(this.intervalId);
}
attachMouseEvents (canvas) {
document.addEventListener('mousemove', this.onMouseMove);
@@ -95,6 +119,13 @@ class Stage extends React.Component {
(nativeSize[1] / this.rect.height) * (y - (this.rect.height / 2))
];
}
getColorInfo (x, y) {
return {
x: x,
y: y,
...this.renderer.extractColor(x, y, colorPickerRadius)
};
}
handleDoubleClick (e) {
const {x, y} = getEventXY(e);
// Set editing target from cursor position, if clicking on a sprite.
@@ -108,6 +139,11 @@ class Stage extends React.Component {
onMouseMove (e) {
const {x, y} = getEventXY(e);
const mousePosition = [x - this.rect.left, y - this.rect.top];
// Set the pickX/Y for the color picker loop to pick up
this.pickX = mousePosition[0];
this.pickY = mousePosition[1];
if (this.state.mouseDownTimeoutId !== null) {
this.cancelMouseDownTimeout();
if (this.state.mouseDown && !this.state.isDragging) {
@@ -173,6 +209,16 @@ class Stage extends React.Component {
if (e.preventDefault) {
e.preventDefault();
}
if (this.props.isColorPicking) {
const {r, g, b} = this.state.colorInfo.color;
const componentToString = c => {
const hex = c.toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
const colorString = `#${componentToString(r)}${componentToString(g)}${componentToString(b)}`;
this.props.onDeactivateColorPicker(colorString);
this.setState({colorInfo: null});
}
}
cancelMouseDownTimeout () {
if (this.state.mouseDownTimeoutId !== null) {
@@ -207,11 +253,13 @@ class Stage extends React.Component {
render () {
const {
vm, // eslint-disable-line no-unused-vars
onActivateColorPicker, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
<StageComponent
canvasRef={this.setCanvas}
colorInfo={this.state.colorInfo}
height={this.props.height}
isZoomed={this.state.isZoomed}
width={this.props.width}
@@ -224,15 +272,25 @@ class Stage extends React.Component {
Stage.propTypes = {
height: PropTypes.number,
isColorPicking: PropTypes.bool,
isZoomed: PropTypes.bool,
onActivateColorPicker: PropTypes.func,
onDeactivateColorPicker: PropTypes.func,
vm: PropTypes.instanceOf(VM).isRequired,
width: PropTypes.number
};
const mapStateToProps = state => ({
isColorPicking: state.colorPicker.active,
isZoomed: state.isZoomed
});
const mapDispatchToProps = dispatch => ({
onActivateColorPicker: () => dispatch(activateColorPicker()),
onDeactivateColorPicker: color => dispatch(deactivateColorPicker(color))
});
export default connect(
mapStateToProps
mapStateToProps,
mapDispatchToProps
)(Stage);
+40
View File
@@ -0,0 +1,40 @@
const ACTIVATE_COLOR_PICKER = 'scratch-gui/color-picker/ACTIVATE_COLOR_PICKER';
const DEACTIVATE_COLOR_PICKER = 'scratch-gui/color-picker/DEACTIVATE_COLOR_PICKER';
const SET_CALLBACK = 'scratch-gui/color-picker/SET_CALLBACK';
const initialState = {
active: false,
callback: () => {
throw new Error('Color picker callback not initialized');
}
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case ACTIVATE_COLOR_PICKER:
return Object.assign({}, state, {active: true, callback: action.callback});
case DEACTIVATE_COLOR_PICKER:
// Can be called without a string to deactivate without setting color
// i.e. when clicking on the modal background
if (typeof action.color === 'string') {
state.callback(action.color);
}
return Object.assign({}, state, {active: false});
case SET_CALLBACK:
return Object.assign({}, state, {callback: action.callback});
default:
return state;
}
};
const activateColorPicker = callback => ({type: ACTIVATE_COLOR_PICKER, callback: callback});
const deactivateColorPicker = color => ({type: DEACTIVATE_COLOR_PICKER, color: color});
const setCallback = callback => ({type: SET_CALLBACK, callback: callback});
export {
reducer as default,
activateColorPicker,
deactivateColorPicker,
setCallback
};
+2 -1
View File
@@ -1,4 +1,5 @@
import {combineReducers} from 'redux';
import colorPickerReducer from './color-picker';
import intlReducer from './intl';
import modalReducer from './modals';
import monitorReducer from './monitors';
@@ -7,8 +8,8 @@ import toolboxReducer from './toolbox';
import vmReducer from './vm';
import zoomReducer from './zoom';
export default combineReducers({
colorPicker: colorPickerReducer,
intl: intlReducer,
isZoomed: zoomReducer,
modals: modalReducer,