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:
commit
e3221cecd9
9 files changed
+299
-40
No files matched your search
@@ -0,0 +1,5 @@
|
||||
.color-picker {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -11,7 +11,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.stage-menu-wrapper {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user