Skip to content
Snippets Groups Projects
Commit 65e01398 authored by Paul Kaplan's avatar Paul Kaplan
Browse files

WIP initial prototype of eye dropper

parent 5111c997
No related branches found
No related tags found
No related merge requests found
.color-picker {
position: absolute;
border-radius: 100%;
border: 1px solid #222;
}
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;
...@@ -13,15 +13,32 @@ ...@@ -13,15 +13,32 @@
background-color: transparent; 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 { .stage-wrapper {
position: relative; position: relative;
} }
.monitor-wrapper { .monitor-wrapper, .color-picker-wrapper {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
overflow: hidden;
} }
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import Box from '../box/box.jsx'; import Box from '../box/box.jsx';
import Loupe from '../loupe/loupe.jsx';
import MonitorList from '../../containers/monitor-list.jsx'; import MonitorList from '../../containers/monitor-list.jsx';
import styles from './stage.css'; import styles from './stage.css';
...@@ -10,27 +12,50 @@ const StageComponent = props => { ...@@ -10,27 +12,50 @@ const StageComponent = props => {
canvasRef, canvasRef,
width, width,
height, height,
colorInfo,
onDeactivateColorPicker,
isColorPicking,
...boxProps ...boxProps
} = props; } = props;
return ( return (
<Box className={styles.stageWrapper}> <div>
<Box <Box
className={styles.stage} className={classNames(styles.stageWrapper, {
componentRef={canvasRef} [styles.withColorPicker]: isColorPicking
element="canvas" })}
height={height} >
width={width} <Box
{...boxProps} className={styles.stage}
/> componentRef={canvasRef}
<Box className={styles.monitorWrapper}> element="canvas"
<MonitorList /> height={height}
width={width}
{...boxProps}
/>
<Box className={styles.monitorWrapper}>
<MonitorList />
</Box>
{isColorPicking && colorInfo ? (
<Box className={styles.colorPickerWrapper}>
<Loupe colorInfo={colorInfo} />
</Box>
) : null}
</Box> </Box>
</Box> {isColorPicking ? (
<Box
className={styles.colorPickerBackground}
onClick={onDeactivateColorPicker}
/>
) : null}
</div>
); );
}; };
StageComponent.propTypes = { StageComponent.propTypes = {
canvasRef: PropTypes.func, canvasRef: PropTypes.func,
colorInfo: Loupe.propTypes.colorInfo,
height: PropTypes.number, height: PropTypes.number,
isColorPicking: PropTypes.bool,
onDeactivateColorPicker: PropTypes.func,
width: PropTypes.number width: PropTypes.number
}; };
StageComponent.defaultProps = { StageComponent.defaultProps = {
......
...@@ -11,6 +11,7 @@ import BlocksComponent from '../components/blocks/blocks.jsx'; ...@@ -11,6 +11,7 @@ import BlocksComponent from '../components/blocks/blocks.jsx';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {updateToolbox} from '../reducers/toolbox'; import {updateToolbox} from '../reducers/toolbox';
import {activateColorPicker} from '../reducers/color-picker';
const addFunctionListener = (object, property, callback) => { const addFunctionListener = (object, property, callback) => {
const oldFn = object[property]; const oldFn = object[property];
...@@ -50,6 +51,8 @@ class Blocks extends React.Component { ...@@ -50,6 +51,8 @@ class Blocks extends React.Component {
this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100); this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
} }
componentDidMount () { componentDidMount () {
this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker;
const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options); const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
...@@ -219,14 +222,17 @@ class Blocks extends React.Component { ...@@ -219,14 +222,17 @@ class Blocks extends React.Component {
this.setState({prompt: null}); this.setState({prompt: null});
} }
render () { render () {
/* eslint-disable no-unused-vars */
const { const {
options, // eslint-disable-line no-unused-vars options,
vm, // eslint-disable-line no-unused-vars vm,
isVisible, // eslint-disable-line no-unused-vars isVisible,
onExtensionAdded, // eslint-disable-line no-unused-vars onActivateColorPicker,
toolboxXML, // eslint-disable-line no-unused-vars onExtensionAdded,
toolboxXML,
...props ...props
} = this.props; } = this.props;
/* eslint-enable no-unused-vars */
return ( return (
<div> <div>
<BlocksComponent <BlocksComponent
...@@ -249,6 +255,7 @@ class Blocks extends React.Component { ...@@ -249,6 +255,7 @@ class Blocks extends React.Component {
Blocks.propTypes = { Blocks.propTypes = {
isVisible: PropTypes.bool, isVisible: PropTypes.bool,
onActivateColorPicker: PropTypes.func,
onExtensionAdded: PropTypes.func, onExtensionAdded: PropTypes.func,
options: PropTypes.shape({ options: PropTypes.shape({
media: PropTypes.string, media: PropTypes.string,
...@@ -311,6 +318,7 @@ const mapStateToProps = state => ({ ...@@ -311,6 +318,7 @@ const mapStateToProps = state => ({
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onActivateColorPicker: callback => dispatch(activateColorPicker(callback)),
onExtensionAdded: toolboxXML => { onExtensionAdded: toolboxXML => {
dispatch(updateToolbox(toolboxXML)); dispatch(updateToolbox(toolboxXML));
} }
......
...@@ -3,10 +3,19 @@ import PropTypes from 'prop-types'; ...@@ -3,10 +3,19 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Renderer from 'scratch-render'; import Renderer from 'scratch-render';
import VM from 'scratch-vm'; import VM from 'scratch-vm';
import {connect} from 'react-redux';
import {getEventXY} from '../lib/touch-utils'; import {getEventXY} from '../lib/touch-utils';
import StageComponent from '../components/stage/stage.jsx'; import StageComponent from '../components/stage/stage.jsx';
import {
activateColorPicker,
deactivateColorPicker
} from '../reducers/color-picker';
const colorPickerRadius = 20;
class Stage extends React.Component { class Stage extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
...@@ -28,7 +37,8 @@ class Stage extends React.Component { ...@@ -28,7 +37,8 @@ class Stage extends React.Component {
mouseDownPosition: null, mouseDownPosition: null,
isDragging: false, isDragging: false,
dragOffset: null, dragOffset: null,
dragId: null dragId: null,
colorInfo: null
}; };
} }
componentDidMount () { componentDidMount () {
...@@ -38,8 +48,11 @@ class Stage extends React.Component { ...@@ -38,8 +48,11 @@ class Stage extends React.Component {
this.renderer = new Renderer(this.canvas); this.renderer = new Renderer(this.canvas);
this.props.vm.attachRenderer(this.renderer); this.props.vm.attachRenderer(this.renderer);
} }
shouldComponentUpdate (nextProps) { shouldComponentUpdate (nextProps, nextState) {
return this.props.width !== nextProps.width || this.props.height !== nextProps.height; return this.props.width !== nextProps.width ||
this.props.height !== nextProps.height ||
this.props.isColorPicking !== nextProps.isColorPicking ||
this.state.colorInfo !== nextState.colorInfo;
} }
componentWillUnmount () { componentWillUnmount () {
this.detachMouseEvents(this.canvas); this.detachMouseEvents(this.canvas);
...@@ -79,6 +92,13 @@ class Stage extends React.Component { ...@@ -79,6 +92,13 @@ class Stage extends React.Component {
(nativeSize[1] / this.rect.height) * (y - (this.rect.height / 2)) (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) { handleDoubleClick (e) {
const {x, y} = getEventXY(e); const {x, y} = getEventXY(e);
// Set editing target from cursor position, if clicking on a sprite. // Set editing target from cursor position, if clicking on a sprite.
...@@ -113,6 +133,10 @@ class Stage extends React.Component { ...@@ -113,6 +133,10 @@ class Stage extends React.Component {
canvasHeight: this.rect.height canvasHeight: this.rect.height
}; };
this.props.vm.postIOData('mouse', coordinates); this.props.vm.postIOData('mouse', coordinates);
if (this.props.isColorPicking) {
this.setState({colorInfo: this.getColorInfo(mousePosition[0], mousePosition[1])});
}
} }
onMouseUp (e) { onMouseUp (e) {
const {x, y} = getEventXY(e); const {x, y} = getEventXY(e);
...@@ -157,6 +181,16 @@ class Stage extends React.Component { ...@@ -157,6 +181,16 @@ class Stage extends React.Component {
if (e.preventDefault) { if (e.preventDefault) {
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 () { cancelMouseDownTimeout () {
if (this.state.mouseDownTimeoutId !== null) { if (this.state.mouseDownTimeoutId !== null) {
...@@ -191,11 +225,13 @@ class Stage extends React.Component { ...@@ -191,11 +225,13 @@ class Stage extends React.Component {
render () { render () {
const { const {
vm, // eslint-disable-line no-unused-vars vm, // eslint-disable-line no-unused-vars
onActivateColorPicker, // eslint-disable-line no-unused-vars
...props ...props
} = this.props; } = this.props;
return ( return (
<StageComponent <StageComponent
canvasRef={this.setCanvas} canvasRef={this.setCanvas}
colorInfo={this.state.colorInfo}
onDoubleClick={this.handleDoubleClick} onDoubleClick={this.handleDoubleClick}
{...props} {...props}
/> />
...@@ -205,8 +241,23 @@ class Stage extends React.Component { ...@@ -205,8 +241,23 @@ class Stage extends React.Component {
Stage.propTypes = { Stage.propTypes = {
height: PropTypes.number, height: PropTypes.number,
isColorPicking: PropTypes.bool,
onActivateColorPicker: PropTypes.func,
onDeactivateColorPicker: PropTypes.func,
vm: PropTypes.instanceOf(VM).isRequired, vm: PropTypes.instanceOf(VM).isRequired,
width: PropTypes.number width: PropTypes.number
}; };
export default Stage; const mapStateToProps = state => ({
isColorPicking: state.colorPicker.active
});
const mapDispatchToProps = dispatch => ({
onActivateColorPicker: () => dispatch(activateColorPicker()),
onDeactivateColorPicker: color => dispatch(deactivateColorPicker(color))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Stage);
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
};
import {combineReducers} from 'redux'; import {combineReducers} from 'redux';
import colorPickerReducer from './color-picker';
import intlReducer from './intl'; import intlReducer from './intl';
import modalReducer from './modals'; import modalReducer from './modals';
import monitorReducer from './monitors'; import monitorReducer from './monitors';
...@@ -6,8 +7,8 @@ import targetReducer from './targets'; ...@@ -6,8 +7,8 @@ import targetReducer from './targets';
import toolboxReducer from './toolbox'; import toolboxReducer from './toolbox';
import vmReducer from './vm'; import vmReducer from './vm';
export default combineReducers({ export default combineReducers({
colorPicker: colorPickerReducer,
intl: intlReducer, intl: intlReducer,
modals: modalReducer, modals: modalReducer,
monitors: monitorReducer, monitors: monitorReducer,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment