Add basic eraser and brush size picker

this implements the first version of #36 and #37, in which the brush size picker is a numeric input field rather than a slider.
This commit is contained in:
Matthew Taylor 2017-10-25 13:37:41 -04:00
parent 706a9c101d
commit ea36e10577
17 changed files with 308 additions and 35 deletions

View file

@ -1,5 +1,5 @@
$border-radius: .25rem; @import "../../css/units";
.button-group { .button-group {
padding: 0 1rem; padding: 0 $grid-unit;
} }

View file

@ -39,6 +39,11 @@ export default function (Input) {
this.setState({value: null}); this.setState({value: null});
} }
handleChange (e) { handleChange (e) {
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: e.target.value}); this.setState({value: e.target.value});
} }
render () { render () {

View file

@ -10,7 +10,7 @@ See https://github.com/LLK/scratch-paint/issues/13 */
padding: 0 0.75rem; padding: 0 0.75rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 0.625rem; font-size: 0.75rem;
font-weight: bold; font-weight: bold;
color: $text-primary; color: $text-primary;
@ -36,8 +36,8 @@ See https://github.com/LLK/scratch-paint/issues/13 */
} }
.input-form:focus { .input-form:focus {
border-color: #4c97ff; border-color: $motion-primary;
box-shadow: inset 0 0 0 -2px rgba(0, 0, 0, 0.1); box-shadow: 0 0 0 $grid-unit $motion-transparent;
} }
.input-small { .input-small {

View file

@ -13,7 +13,7 @@ See https://github.com/LLK/scratch-paint/issues/13 */
.input-label, .input-label-secondary { .input-label, .input-label-secondary {
font-size: 0.625rem; font-size: 0.625rem;
margin-right: calc($space / 2); margin-right: calc(2 * $grid-unit);
user-select: none; user-select: none;
cursor: default; cursor: default;
} }

View file

@ -1,5 +1,5 @@
@import '../../css/units.css'; @import '../../css/units.css';
.input-group + .input-group { .input-group + .input-group {
margin-left: calc(2 * $space); margin-left: calc(3 * $grid-unit);
} }

View file

@ -1,5 +1,5 @@
@import "../../../css/colors.css"; @import "../../css/colors.css";
@import "../../../css/units.css"; @import "../../css/units.css";
$border-radius: 0.25rem; $border-radius: 0.25rem;
@ -16,7 +16,7 @@ $border-radius: 0.25rem;
} }
.mod-edit-field:active { .mod-edit-field:active {
background-color: $ui-background-blue; background-color: $motion-transparent;
} }
.edit-field-icon { .edit-field-icon {
@ -28,4 +28,6 @@ $border-radius: 0.25rem;
.edit-field-title { .edit-field-title {
display: block; display: block;
margin-top: .125rem;
font-size: .625rem;
} }

View file

@ -1,14 +1,19 @@
/* @todo This file should be pulled out into a shared library with scratch-gui,
consolidating this component with icon-button.jsx in gui.
See #13 */
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Button from '../../button/button.jsx'; import Button from '../button/button.jsx';
import styles from './edit-field-button.css'; import styles from './labeled-icon-button.css';
const EditFieldButton = props => ( const LabeledIconButton = props => (
<Button <Button
className={classNames(props.className, styles.modEditField)} className={classNames(props.className, styles.modEditField)}
disabled={props.disabled}
onClick={props.onClick} onClick={props.onClick}
> >
<img <img
@ -20,12 +25,13 @@ const EditFieldButton = props => (
</Button> </Button>
); );
EditFieldButton.propTypes = { LabeledIconButton.propTypes = {
className: PropTypes.string, className: PropTypes.string,
disabled: PropTypes.string,
imgAlt: PropTypes.string.isRequired, imgAlt: PropTypes.string.isRequired,
imgSrc: PropTypes.string.isRequired, imgSrc: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
title: PropTypes.string.isRequired title: PropTypes.string.isRequired
}; };
export default EditFieldButton; export default LabeledIconButton;

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>curved-point</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="curved-point">
<path d="M2,15 C2,10.5818452 5.58151214,7 10.000744,7 C14.4184879,7 18,10.5818452 18,15" id="Stroke-3" stroke="#4C97FF" stroke-width="0.75" fill-opacity="0.25" fill="#4C97FF" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M3,7 L17,7" id="Stroke-7" stroke="#4C97FF" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"></path>
<circle id="Oval-4" fill-opacity="0.25" fill="#4C97FF" cx="10" cy="7" r="3"></circle>
<circle id="Oval-4" fill="#4C97FF" cx="10" cy="7" r="2"></circle>
<circle id="Oval-5" fill="#4C97FF" cx="3" cy="7" r="1"></circle>
<circle id="Oval-5-Copy" fill="#4C97FF" cx="17" cy="7" r="1"></circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>flip-horizontal</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="flip-horizontal">
<g transform="translate(2.000000, 3.000000)">
<circle id="Oval" fill="#575E75" opacity="0.5" cx="8" cy="0.75" r="1"></circle>
<circle id="Oval" fill="#575E75" opacity="0.5" cx="8" cy="13.25" r="1"></circle>
<circle id="Oval-Copy" fill="#575E75" opacity="0.5" cx="8" cy="3.875" r="1"></circle>
<circle id="Oval-Copy-2" fill="#575E75" opacity="0.5" cx="8" cy="7" r="1"></circle>
<circle id="Oval-Copy-3" fill="#575E75" opacity="0.5" cx="8" cy="10.125" r="1"></circle>
<path d="M16,3.08425423 L16,10.9157458 C16,11.4342626 15.2574491,11.6956996 14.8235798,11.3282353 L10.2019293,7.41103711 C9.93269025,7.18445835 9.93269025,6.81408922 10.2019293,6.58751046 L14.8235798,2.67176469 C15.2574491,2.30430042 16,2.56573745 16,3.08425423" id="Fill-11" fill="#4C97FF" opacity="0.5"></path>
<path d="M0,10.9157458 L0,3.08425423 C0,2.56573745 0.742550911,2.30430042 1.17470525,2.67176469 L5.79807074,6.58896289 C6.06730975,6.81554165 6.06730975,7.18591078 5.79807074,7.41248954 L1.17470525,11.3282353 C0.742550911,11.6956996 0,11.4342626 0,10.9157458" id="Fill-14" fill="#4C97FF"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>flip-vertical</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="flip-vertical">
<g id="flip-horizontal" transform="translate(10.000000, 10.000000) rotate(90.000000) translate(-10.000000, -10.000000) translate(2.000000, 3.000000)">
<circle id="Oval" fill="#575E75" opacity="0.5" cx="8" cy="0.75" r="1"></circle>
<circle id="Oval" fill="#575E75" opacity="0.5" cx="8" cy="13.25" r="1"></circle>
<circle id="Oval-Copy" fill="#575E75" opacity="0.5" cx="8" cy="3.875" r="1"></circle>
<circle id="Oval-Copy-2" fill="#575E75" opacity="0.5" cx="8" cy="7" r="1"></circle>
<circle id="Oval-Copy-3" fill="#575E75" opacity="0.5" cx="8" cy="10.125" r="1"></circle>
<path d="M16,3.08425423 L16,10.9157458 C16,11.4342626 15.2574491,11.6956996 14.8235798,11.3282353 L10.2019293,7.41103711 C9.93269025,7.18445835 9.93269025,6.81408922 10.2019293,6.58751046 L14.8235798,2.67176469 C15.2574491,2.30430042 16,2.56573745 16,3.08425423" id="Fill-11" fill="#4C97FF" opacity="0.5"></path>
<path d="M0,10.9157458 L0,3.08425423 C0,2.56573745 0.742550911,2.30430042 1.17470525,2.67176469 L5.79807074,6.58896289 C6.06730975,6.81554165 6.06730975,7.18591078 5.79807074,7.41248954 L1.17470525,11.3282353 C0.742550911,11.6956996 0,11.4342626 0,10.9157458" id="Fill-14" fill="#4C97FF"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,13 @@
@import "../../css/units.css";
.mode-tools {
display: flex;
min-height: 3rem;
align-items: center;
}
.mode-tools-icon {
margin-right: calc(2 * $grid-unit);
width: 2rem;
height: 2rem;
}

View file

@ -0,0 +1,170 @@
import classNames from 'classnames';
import {connect} from 'react-redux';
import Popover from 'react-popover';
import PropTypes from 'prop-types';
import React from 'react';
import {changeBrushSize} from '../../reducers/brush-mode';
import {changeBrushSize as changeEraserSize} from '../../reducers/eraser-mode';
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
import {injectIntl, intlShape} from 'react-intl';
import Input from '../forms/input.jsx';
import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
import Modes from '../../modes/modes';
import Slider from '../forms/slider.jsx';
import styles from './mode-tools.css';
import brushIcon from '../brush-mode/brush.svg';
import curvedPointIcon from './curved-point.svg';
import eraserIcon from '../eraser-mode/eraser.svg';
import flipHorizontalIcon from './flip-horizontal.svg';
import flipVerticalIcon from './flip-vertical.svg';
import straightPointIcon from './straight-point.svg';
import {MAX_STROKE_WIDTH} from '../../reducers/stroke-width';
const BufferedInput = BufferedInputHOC(Input);
const ModeToolsComponent = props => {
const brushMessage = props.intl.formatMessage({
defaultMessage: 'Brush',
description: 'Label for the brush tool',
id: 'paint.brushMode.brush'
});
const eraserMessage = props.intl.formatMessage({
defaultMessage: 'Eraser',
description: 'Label for the eraser tool',
id: 'paint.eraserMode.eraser'
});
switch (props.mode) {
case Modes.BRUSH:
return (
<div className={classNames(props.className, styles.modeTools)}>
<div>
<img
alt={brushMessage}
className={styles.modeToolsIcon}
src={brushIcon}
/>
</div>
<BufferedInput
small
max={MAX_STROKE_WIDTH}
min="0"
type="number"
value={props.brushValue}
onSubmit={props.onBrushSliderChange}
/>
<Popover
body={
<Slider
value={props.brushValue}
onChange={props.onBrushSliderChange}
/>
}
/>
</div>
);
case Modes.ERASER:
return (
<div className={classNames(props.className, styles.modeTools)}>
<div>
<img
alt={eraserMessage}
className={styles.modeToolsIcon}
src={eraserIcon}
/>
</div>
<BufferedInput
small
max={MAX_STROKE_WIDTH}
min="0"
type="number"
value={props.eraserValue}
onSubmit={props.onEraserSliderChange}
/>
<Popover
body={
<Slider
value={props.eraserValue}
onChange={props.onEraserSliderChange}
/>
}
/>
</div>
);
case Modes.RESHAPE:
return (
<div className={classNames(props.className, styles.modeTools)}>
<LabeledIconButton
disabled
imgAlt="Curved Point Icon"
imgSrc={curvedPointIcon}
title="Curved"
onClick={function () {}}
/>
<LabeledIconButton
disabled
imgAlt="Straight Point Icon"
imgSrc={straightPointIcon}
title="Pointed"
onClick={function () {}}
/>
</div>
);
case Modes.SELECT:
return (
<div className={classNames(props.className, styles.modeTools)}>
<LabeledIconButton
disabled
imgAlt="Flip Horizontal Icon"
imgSrc={flipHorizontalIcon}
title="Flip Horizontal"
onClick={function () {}}
/>
<LabeledIconButton
disabled
imgAlt="Flip Vertical Icon"
imgSrc={flipVerticalIcon}
title="Flip Vertical"
onClick={function () {}}
/>
</div>
);
default:
// Leave empty for now, if mode not supported
return (
<div className={classNames(props.className, styles.modeTools)} />
);
}
};
ModeToolsComponent.propTypes = {
brushValue: PropTypes.number,
className: PropTypes.string,
eraserValue: PropTypes.number,
intl: intlShape.isRequired,
mode: PropTypes.string.isRequired,
onBrushSliderChange: PropTypes.func,
onEraserSliderChange: PropTypes.func
};
const mapStateToProps = state => ({
mode: state.scratchPaint.mode,
brushValue: state.scratchPaint.brushMode.brushSize,
eraserValue: state.scratchPaint.eraserMode.brushSize
});
const mapDispatchToProps = dispatch => ({
onBrushSliderChange: brushSize => {
dispatch(changeBrushSize(brushSize));
},
onEraserSliderChange: eraserSize => {
dispatch(changeEraserSize(eraserSize));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(injectIntl(ModeToolsComponent));

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>straight-point</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="straight-point" fill="#4C97FF">
<polyline id="Path-2" stroke="#4C97FF" stroke-width="0.75" fill-opacity="0.25" stroke-linecap="round" stroke-linejoin="round" points="2 15 10 7 18 15"></polyline>
<circle id="Oval-4" fill-opacity="0.25" cx="10" cy="7" r="3"></circle>
<circle id="Oval-4" cx="10" cy="7" r="2"></circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View file

@ -4,7 +4,7 @@
.editor-container { .editor-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: calc(2 * $space); padding: calc(4 * $grid-unit);
} }
.row { .row {
@ -15,22 +15,22 @@
.editor-container-top { .editor-container-top {
border-bottom: 1px dashed $ui-pane-border; border-bottom: 1px dashed $ui-pane-border;
padding-bottom: 1rem; padding-bottom: calc(2 * $grid-unit);
} }
.top-align-row { .top-align-row {
display: flex; display: flex;
padding-top:20px; padding-top: calc(5 * $grid-unit);
flex-direction: row; flex-direction: row;
} }
.row + .row { .row + .row {
margin-top: calc(2 * $space); margin-top: calc(2 * $grid-unit);
} }
.mod-dashed-border { .mod-dashed-border {
border-right: 1px dashed $ui-pane-border; border-right: 1px dashed $ui-pane-border;
padding-right: calc(2 * $space); padding-right: calc(3 * $grid-unit);
} }
$border-radius: 0.25rem; $border-radius: 0.25rem;
@ -40,7 +40,7 @@ $border-radius: 0.25rem;
border: 1px solid $ui-pane-border; border: 1px solid $ui-pane-border;
border-radius: 0; border-radius: 0;
border-left: none; border-left: none;
padding: 0.5rem; padding: calc(2 * $grid-unit);
} }
.button-group-button:active { .button-group-button:active {
@ -59,13 +59,13 @@ $border-radius: 0.25rem;
} }
.button-group-button-icon { .button-group-button-icon {
width: 1.5rem; width: 1.25rem;
height: 1.5rem; height: 1.25rem;
vertical-align: middle; vertical-align: middle;
} }
.mod-mode-tools { .mod-mode-tools {
margin-left: calc(2 * $space); margin-left: calc(3 * $grid-unit);
} }
.canvas-container { .canvas-container {
@ -79,7 +79,7 @@ $border-radius: 0.25rem;
.mode-selector { .mode-selector {
display: flex; display: flex;
margin-right: .5rem; margin-right: calc(2 * $grid-unit);
max-width: 5.5rem; max-width: 5.5rem;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -8,10 +8,11 @@ import PaperCanvas from '../../containers/paper-canvas.jsx';
import Button from '../button/button.jsx'; import Button from '../button/button.jsx';
import ButtonGroup from '../button-group/button-group.jsx'; import ButtonGroup from '../button-group/button-group.jsx';
import BrushMode from '../../containers/brush-mode.jsx'; import BrushMode from '../../containers/brush-mode.jsx';
import EditFieldButton from './edit-field-button/edit-field-button.jsx';
import EraserMode from '../../containers/eraser-mode.jsx'; import EraserMode from '../../containers/eraser-mode.jsx';
import InputGroup from '../input-group/input-group.jsx'; import InputGroup from '../input-group/input-group.jsx';
import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
import LineMode from '../../containers/line-mode.jsx'; import LineMode from '../../containers/line-mode.jsx';
import ModeToolsComponent from '../mode-tools/mode-tools.jsx';
import OvalMode from '../../containers/oval-mode.jsx'; import OvalMode from '../../containers/oval-mode.jsx';
import PenMode from '../../containers/pen-mode.jsx'; import PenMode from '../../containers/pen-mode.jsx';
import RectMode from '../../containers/rect-mode.jsx'; import RectMode from '../../containers/rect-mode.jsx';
@ -103,13 +104,13 @@ class PaintEditorComponent extends React.Component {
{/* Group/Ungroup */} {/* Group/Ungroup */}
<InputGroup className={styles.modDashedBorder}> <InputGroup className={styles.modDashedBorder}>
<EditFieldButton <LabeledIconButton
imgAlt="Group Icon" imgAlt="Group Icon"
imgSrc={groupIcon} imgSrc={groupIcon}
title="Group" title="Group"
onClick={this.props.onGroup} onClick={this.props.onGroup}
/> />
<EditFieldButton <LabeledIconButton
imgAlt="Ungroup Icon" imgAlt="Ungroup Icon"
imgSrc={ungroupIcon} imgSrc={ungroupIcon}
title="Ungroup" title="Ungroup"
@ -119,13 +120,13 @@ class PaintEditorComponent extends React.Component {
{/* Forward/Backward */} {/* Forward/Backward */}
<InputGroup className={styles.modDashedBorder}> <InputGroup className={styles.modDashedBorder}>
<EditFieldButton <LabeledIconButton
imgAlt="Send Forward Icon" imgAlt="Send Forward Icon"
imgSrc={sendForwardIcon} imgSrc={sendForwardIcon}
title="Forward" title="Forward"
onClick={this.props.onSendForward} onClick={this.props.onSendForward}
/> />
<EditFieldButton <LabeledIconButton
imgAlt="Send Backward Icon" imgAlt="Send Backward Icon"
imgSrc={sendBackwardIcon} imgSrc={sendBackwardIcon}
title="Backward" title="Backward"
@ -135,13 +136,13 @@ class PaintEditorComponent extends React.Component {
{/* Front/Back */} {/* Front/Back */}
<InputGroup> <InputGroup>
<EditFieldButton <LabeledIconButton
imgAlt="Send to Front Icon" imgAlt="Send to Front Icon"
imgSrc={sendFrontIcon} imgSrc={sendFrontIcon}
title="Front" title="Front"
onClick={this.props.onSendToFront} onClick={this.props.onSendToFront}
/> />
<EditFieldButton <LabeledIconButton
imgAlt="Send to Back Icon" imgAlt="Send to Back Icon"
imgSrc={sendBackIcon} imgSrc={sendBackIcon}
title="Back" title="Back"
@ -151,7 +152,7 @@ class PaintEditorComponent extends React.Component {
{/* To be rotation point */} {/* To be rotation point */}
{/* <InputGroup> {/* <InputGroup>
<EditFieldButton <LabeledIconButton
imgAlt="Rotation Point Icon" imgAlt="Rotation Point Icon"
imgSrc={rotationPointIcon} imgSrc={rotationPointIcon}
title="Rotation Point" title="Rotation Point"
@ -177,7 +178,7 @@ class PaintEditorComponent extends React.Component {
/> />
</div> </div>
<InputGroup className={styles.modModeTools}> <InputGroup className={styles.modModeTools}>
Mode tools <ModeToolsComponent />
</InputGroup> </InputGroup>
</div> </div>
</div> </div>

View file

@ -14,7 +14,7 @@ $border-radius: .25rem;
} }
.mod-tool-select.is-selected { .mod-tool-select.is-selected {
background-color: $ui-background-blue; background-color: $motion-transparent;
} }
.mod-tool-select:focus { .mod-tool-select:focus {

View file

@ -2,7 +2,12 @@
@todo This file is copied from GUI and should be pulled out into a shared library. @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 */ See https://github.com/LLK/scratch-paint/issues/13 */
/* ACTUALLY, THIS IS EDITED ;)
THIS WAS CHANGED ON 10/25/2017 BY @mewtaylor TO ADD A VARIABLE FOR THE SMALLEST
GRID UNITS.*/
$space: 0.5rem; $space: 0.5rem;
$grid-unit: .25rem;
$sprites-per-row: 5; $sprites-per-row: 5;