Merge pull request #10 from fsih/structure

UI skeleton for the paint editor
This commit is contained in:
DD Liu 2017-09-11 11:17:00 -04:00 committed by GitHub
commit 0585f10062
11 changed files with 475 additions and 28 deletions

View file

@ -0,0 +1,64 @@
/* DO NOT EDIT
@todo This file is copied from GUI and should be pulled out into a shared library.
See https://github.com/LLK/scratch-paint/issues/13 */
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
/**
* Higher Order Component to manage inputs that submit on blur and <enter>
* @param {React.Component} Input text input that consumes onChange, onBlur, onKeyPress
* @returns {React.Component} Buffered input that calls onSubmit on blur and <enter>
*/
export default function (Input) {
class BufferedInput extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleChange',
'handleKeyPress',
'handleFlush'
]);
this.state = {
value: null
};
}
handleKeyPress (e) {
if (e.key === 'Enter') {
this.handleFlush();
e.target.blur();
}
}
handleFlush () {
const isNumeric = typeof this.props.value === 'number';
const validatesNumeric = isNumeric ? !isNaN(this.state.value) : true;
if (this.state.value !== null && validatesNumeric) {
this.props.onSubmit(isNumeric ? Number(this.state.value) : this.state.value);
}
this.setState({value: null});
}
handleChange (e) {
this.setState({value: e.target.value});
}
render () {
const bufferedValue = this.state.value === null ? this.props.value : this.state.value;
return (
<Input
{...this.props}
value={bufferedValue}
onBlur={this.handleFlush}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
);
}
}
BufferedInput.propTypes = {
onSubmit: PropTypes.func.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
return BufferedInput;
}

View file

@ -0,0 +1,46 @@
/* DO NOT EDIT
@todo This file is copied from GUI and should be pulled out into a shared library.
See https://github.com/LLK/scratch-paint/issues/13 */
@import "../../css/units.css";
@import "../../css/colors.css";
.input-form {
height: 2rem;
padding: 0 0.75rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 0.625rem;
font-weight: bold;
color: $text-primary;
border-width: 1px;
border-style: solid;
border-color: $form-border;
border-radius: 2rem;
outline: none;
cursor: text;
transition: 0.25s ease-out; /* @todo: standardize with var */
box-shadow: none;
/*
For truncating overflowing text gracefully
Min-width is for a bug: https://css-tricks.com/flexbox-truncated-text
@todo: move this out into a mixin or a helper component
*/
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.input-form:focus {
border-color: #4c97ff;
box-shadow: inset 0 0 0 -2px rgba(0, 0, 0, 0.1);
}
.input-small {
width: 3rem;
text-align: center;
}

View file

@ -0,0 +1,31 @@
/* DO NOT EDIT
@todo This file is copied from GUI and should be pulled out into a shared library.
See https://github.com/LLK/scratch-paint/issues/13 */
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import styles from './input.css';
const Input = props => {
const {small, ...componentProps} = props;
return (
<input
{...componentProps}
className={classNames(styles.inputForm, {
[styles.inputSmall]: small
})}
/>
);
};
Input.propTypes = {
small: PropTypes.bool
};
Input.defaultProps = {
small: false
};
export default Input;

View file

@ -0,0 +1,23 @@
/* DO NOT EDIT
@todo This file is copied from GUI and should be pulled out into a shared library.
See https://github.com/LLK/scratch-paint/issues/13 */
@import "../../css/units.css";
@import "../../css/colors.css";
.input-group {
display: inline-flex;
flex-direction: row;
align-items: center;
}
.input-label, .input-label-secondary {
font-size: 0.625rem;
margin-right: calc($space / 2);
user-select: none;
cursor: default;
}
.input-label {
font-weight: bold;
}

View file

@ -0,0 +1,29 @@
/* DO NOT EDIT
@todo This file is copied from GUI and should be pulled out into a shared library.
See https://github.com/LLK/scratch-paint/issues/13 */
import PropTypes from 'prop-types';
import React from 'react';
import styles from './label.css';
const Label = props => (
<label className={styles.inputGroup}>
<span className={props.secondary ? styles.inputLabelSecondary : styles.inputLabel}>
{props.text}
</span>
{props.children}
</label>
);
Label.propTypes = {
children: PropTypes.node,
secondary: PropTypes.bool,
text: PropTypes.string.isRequired
};
Label.defaultProps = {
secondary: false
};
export default Label;

View file

@ -0,0 +1,88 @@
@import "../css/colors.css";
@import "../css/units.css";
.editor-container {
display: flex;
flex-direction: column;
padding: calc(2 * $space);
}
.row {
display: flex;
flex-direction: row;
align-items: center;
}
.top-align-row {
padding-top:20px;
display: flex;
flex-direction: row;
}
.row + .row {
margin-top: calc(2 * $space);
}
.input-group + .input-group {
margin-left: calc(2 * $space);
}
$border-radius: 0.25rem;
.button {
height: 2rem;
padding: 0.25rem;
outline: none;
background: white;
border-radius: $border-radius;
border: 1px solid #ddd;
cursor: pointer;
font-size: 0.85rem;
transition: 0.2s;
}
.button > img {
flex-grow: 1;
max-width: 100%;
max-height: 100%;
min-width: 1.5rem;
}
.button-group {
margin: 0 1rem;
}
.button-group .button {
border-radius: 0;
border-left: none;
}
.button-group .button:last-of-type {
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
.button-group .button:first-of-type {
border-left: 1px solid #ddd;
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
.button:disabled > img {
opacity: 0.25;
}
.canvas-container {
width: 503px;
height: 403px;
background-color: #e8edf1;
border: 1px solid #e8edf1;
border-radius: 2px;
position: relative;
overflow: visible;
}
.mode-selector {
display: flex;
flex-direction: column;
}

View file

@ -6,6 +6,32 @@ import EraserMode from '../containers/eraser-mode.jsx';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import LineMode from '../containers/line-mode.jsx'; import LineMode from '../containers/line-mode.jsx';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import BufferedInputHOC from './forms/buffered-input-hoc.jsx';
import Label from './forms/label.jsx';
import Input from './forms/input.jsx';
import styles from './paint-editor.css';
const BufferedInput = BufferedInputHOC(Input);
const messages = defineMessages({
costume: {
id: 'paint.paintEditor.costume',
description: 'Label for the name of a sound',
defaultMessage: 'Costume'
},
fill: {
id: 'paint.paintEditor.fill',
description: 'Label for the color picker for the fill color',
defaultMessage: 'Fill'
},
outline: {
id: 'paint.paintEditor.outline',
description: 'Label for the color picker for the outline color',
defaultMessage: 'Outline'
}
});
class PaintEditorComponent extends React.Component { class PaintEditorComponent extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
@ -18,44 +44,134 @@ class PaintEditorComponent extends React.Component {
this.setState({canvas: canvas}); this.setState({canvas: canvas});
} }
render () { render () {
// Modes can't work without a canvas, so we don't render them until we have it
if (this.state.canvas) {
return (
<div>
<PaperCanvas
canvasRef={this.setCanvas}
rotationCenterX={this.props.rotationCenterX}
rotationCenterY={this.props.rotationCenterY}
svg={this.props.svg}
/>
<BrushMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg}
/>
<EraserMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg}
/>
<LineMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg}
/>
</div>
);
}
return ( return (
<div> <div className={styles.editorContainer}>
<PaperCanvas canvasRef={this.setCanvas} /> {/* First row */}
<div className={styles.row}>
{/* Name field */}
<div className={styles.inputGroup}>
<Label text={this.props.intl.formatMessage(messages.costume)}>
<BufferedInput
tabIndex="1"
type="text"
value="meow"
/>
</Label>
</div>
{/* Undo/Redo */}
<div className={styles.inputGroup}>
<div className={styles.buttonGroup}>
<button
className={styles.button}
>
Undo
</button>
<button
className={styles.button}
>
Redo
</button>
</div>
</div>
{/* To be Front/back */}
<div className={styles.inputGroup}>
<button
className={styles.button}
>
Front
</button>
<button
className={styles.button}
>
Back
</button>
</div>
{/* To be Group/Ungroup */}
<div className={styles.inputGroup}>
<button
className={styles.button}
>
Group
</button>
<button
className={styles.button}
>
Ungroup
</button>
</div>
</div>
{/* Second Row */}
<div className={styles.row}>
{/* To be fill */}
<div className={styles.inputGroup}>
<Label text={this.props.intl.formatMessage(messages.fill)}>
<BufferedInput
tabIndex="1"
type="text"
value="meow"
/>
</Label>
</div>
{/* To be stroke */}
<div className={styles.inputGroup}>
<Label text={this.props.intl.formatMessage(messages.outline)}>
<BufferedInput
tabIndex="1"
type="text"
value="meow"
/>
</Label>
</div>
<div className={styles.inputGroup}>
Mode tools
</div>
</div>
<div className={styles.topAlignRow}>
{/* Modes */}
{this.state.canvas ? (
<div className={styles.modeSelector}>
<BrushMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg}
/>
<EraserMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg}
/>
<LineMode
canvas={this.state.canvas}
onUpdateSvg={this.props.onUpdateSvg}
/>
</div>
) : null}
{/* Canvas */}
<div className={styles.canvasContainer}>
<PaperCanvas
canvasRef={this.setCanvas}
rotationCenterX={this.props.rotationCenterX}
rotationCenterY={this.props.rotationCenterY}
svg={this.props.svg}
/>
</div>
</div>
</div> </div>
); );
} }
} }
PaintEditorComponent.propTypes = { PaintEditorComponent.propTypes = {
intl: intlShape,
onUpdateSvg: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired,
rotationCenterX: PropTypes.number, rotationCenterX: PropTypes.number,
rotationCenterY: PropTypes.number, rotationCenterY: PropTypes.number,
svg: PropTypes.string svg: PropTypes.string
}; };
export default PaintEditorComponent; export default injectIntl(PaintEditorComponent);

View file

@ -0,0 +1,7 @@
.paper-canvas {
width: 500px;
height: 400px;
margin: auto;
position: relative;
background-color: #fff;
}

View file

@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import paper from 'paper'; import paper from 'paper';
import styles from './paper-canvas.css';
class PaperCanvas extends React.Component { class PaperCanvas extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
@ -60,7 +62,10 @@ class PaperCanvas extends React.Component {
render () { render () {
return ( return (
<canvas <canvas
className={styles.paperCanvas}
height="400px"
ref={this.setCanvas} ref={this.setCanvas}
width="500px"
/> />
); );
} }

23
src/css/colors.css Normal file
View file

@ -0,0 +1,23 @@
/* DO NOT EDIT
@todo This file is copied from GUI and should be pulled out into a shared library.
See https://github.com/LLK/scratch-paint/issues/13 */
$ui-pane-border: #D9D9D9;
$ui-pane-gray: #F9F9F9;
$ui-background-blue: #e8edf1;
$text-primary: #575e75;
$motion-primary: #4C97FF;
$motion-tertiary: #3373CC;
$motion-transparent: hsla(215, 100%, 65%, 0.20);
$red-primary: #FF661A;
$red-tertiary: #E64D00;
$sound-primary: #CF63CF;
$sound-tertiary: #A63FA6;
$control-primary: #FFAB19;
$form-border: #E9EEF2;

15
src/css/units.css Normal file
View file

@ -0,0 +1,15 @@
/* DO NOT EDIT
@todo This file is copied from GUI and should be pulled out into a shared library.
See https://github.com/LLK/scratch-paint/issues/13 */
$space: 0.5rem;
$sprites-per-row: 5;
$menu-bar-height: 3rem;
$sprite-info-height: 6rem;
$stage-menu-height: 2.75rem;
$library-header-height: 4.375rem;
$form-radius: calc($space / 2);