mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Merge pull request #10 from fsih/structure
UI skeleton for the paint editor
This commit is contained in:
commit
0585f10062
11 changed files with 475 additions and 28 deletions
64
src/components/forms/buffered-input-hoc.jsx
Normal file
64
src/components/forms/buffered-input-hoc.jsx
Normal 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;
|
||||||
|
}
|
46
src/components/forms/input.css
Normal file
46
src/components/forms/input.css
Normal 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;
|
||||||
|
}
|
31
src/components/forms/input.jsx
Normal file
31
src/components/forms/input.jsx
Normal 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;
|
23
src/components/forms/label.css
Normal file
23
src/components/forms/label.css
Normal 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;
|
||||||
|
}
|
29
src/components/forms/label.jsx
Normal file
29
src/components/forms/label.jsx
Normal 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;
|
88
src/components/paint-editor.css
Normal file
88
src/components/paint-editor.css
Normal 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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
7
src/containers/paper-canvas.css
Normal file
7
src/containers/paper-canvas.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.paper-canvas {
|
||||||
|
width: 500px;
|
||||||
|
height: 400px;
|
||||||
|
margin: auto;
|
||||||
|
position: relative;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
|
@ -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
23
src/css/colors.css
Normal 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
15
src/css/units.css
Normal 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);
|
Loading…
Reference in a new issue