mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-10 14:42:13 -05:00
commit
99534eb24c
13 changed files with 493 additions and 38 deletions
25
src/components/bit-brush-mode/bit-brush-mode.jsx
Normal file
25
src/components/bit-brush-mode/bit-brush-mode.jsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
|
||||||
|
|
||||||
|
import brushIcon from './brush.svg';
|
||||||
|
|
||||||
|
const BitBrushModeComponent = props => (
|
||||||
|
<ToolSelectComponent
|
||||||
|
imgDescriptor={{
|
||||||
|
defaultMessage: 'Brush',
|
||||||
|
description: 'Label for the brush tool',
|
||||||
|
id: 'paint.brushMode.brush'
|
||||||
|
}}
|
||||||
|
imgSrc={brushIcon}
|
||||||
|
isSelected={props.isSelected}
|
||||||
|
onMouseDown={props.onMouseDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
BitBrushModeComponent.propTypes = {
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
onMouseDown: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BitBrushModeComponent;
|
10
src/components/bit-brush-mode/brush.svg
Normal file
10
src/components/bit-brush-mode/brush.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>brush</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="brush" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M9.50062663,11.4981734 L10.4995734,11.4981734 L10.4995734,14.4990134 L9.49962669,14.4990134 L9.49962669,11.4991734 L6.49978668,11.4991734 L6.49978668,10.4992267 L9.50062663,10.4992267 L9.50062663,11.4981734 Z M13.4992134,3.5 L16.5000533,3.5 L16.5000533,4.49994667 L17.5,4.49994667 L17.5,7.49978668 L16.5000533,7.49978668 L16.5000533,8.4987334 L15.4991067,8.4987334 L15.4991067,9.49868007 L14.49916,9.49868007 L14.49916,10.4986267 L12.4992667,10.4986267 L12.4992667,11.4985734 L11.49932,11.4985734 L11.49932,10.4986267 L10.4993734,10.4986267 L10.4993734,9.49868007 L9.4994267,9.49868007 L9.4994267,7.49978668 L10.4993734,7.49978668 L10.4993734,6.49984001 L11.49932,6.49984001 L11.49932,5.49989334 L12.4992667,5.49989334 L12.4992667,4.49994667 L13.4992134,4.49994667 L13.4992134,3.5 Z M5.49954002,11.4987734 L6.49948669,11.4987734 L6.49948669,12.49972 L7.50043331,12.49972 L7.50043331,13.4996667 L8.50037998,13.4996667 L8.50037998,14.4996134 L9.50032665,14.4996134 L9.50032665,15.49956 L8.50037998,15.49956 L8.50037998,16.4985067 L4.49959336,16.4985067 L4.49959336,15.49956 L3.49964669,15.49956 L3.49964669,13.4996667 L5.49954002,13.4996667 L5.49954002,11.4987734 Z M2.5,13.4990667 L2.5,12.49912 L3.49994667,12.49912 L3.49994667,13.4990667 L2.5,13.4990667 Z" id="Combined-Shape" fill="#575E75"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -6,6 +6,7 @@ import React from 'react';
|
||||||
|
|
||||||
import {changeBrushSize} from '../../reducers/brush-mode';
|
import {changeBrushSize} from '../../reducers/brush-mode';
|
||||||
import {changeBrushSize as changeEraserSize} from '../../reducers/eraser-mode';
|
import {changeBrushSize as changeEraserSize} from '../../reducers/eraser-mode';
|
||||||
|
import {changeBitBrushSize} from '../../reducers/bit-brush-size';
|
||||||
|
|
||||||
import LiveInputHOC from '../forms/live-input-hoc.jsx';
|
import LiveInputHOC from '../forms/live-input-hoc.jsx';
|
||||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||||
|
@ -13,12 +14,15 @@ import Input from '../forms/input.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 LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
|
||||||
import Modes from '../../lib/modes';
|
import Modes from '../../lib/modes';
|
||||||
|
import Formats from '../../lib/format';
|
||||||
|
import {isBitmap} from '../../lib/format';
|
||||||
import styles from './mode-tools.css';
|
import styles from './mode-tools.css';
|
||||||
|
|
||||||
import copyIcon from './icons/copy.svg';
|
import copyIcon from './icons/copy.svg';
|
||||||
import pasteIcon from './icons/paste.svg';
|
import pasteIcon from './icons/paste.svg';
|
||||||
|
|
||||||
import brushIcon from '../brush-mode/brush.svg';
|
import brushIcon from '../brush-mode/brush.svg';
|
||||||
|
import bitBrushIcon from '../bit-brush-mode/brush.svg';
|
||||||
import curvedPointIcon from './icons/curved-point.svg';
|
import curvedPointIcon from './icons/curved-point.svg';
|
||||||
import eraserIcon from '../eraser-mode/eraser.svg';
|
import eraserIcon from '../eraser-mode/eraser.svg';
|
||||||
import flipHorizontalIcon from './icons/flip-horizontal.svg';
|
import flipHorizontalIcon from './icons/flip-horizontal.svg';
|
||||||
|
@ -74,6 +78,12 @@ const ModeToolsComponent = props => {
|
||||||
|
|
||||||
switch (props.mode) {
|
switch (props.mode) {
|
||||||
case Modes.BRUSH:
|
case Modes.BRUSH:
|
||||||
|
/* falls through */
|
||||||
|
case Modes.BIT_BRUSH:
|
||||||
|
{
|
||||||
|
const currentBrushIcon = isBitmap(props.format) ? bitBrushIcon : brushIcon;
|
||||||
|
const currentBrushValue = isBitmap(props.format) ? props.bitBrushSize : props.brushValue;
|
||||||
|
const changeFunction = isBitmap(props.format) ? props.onBitBrushSliderChange : props.onBrushSliderChange;
|
||||||
return (
|
return (
|
||||||
<div className={classNames(props.className, styles.modeTools)}>
|
<div className={classNames(props.className, styles.modeTools)}>
|
||||||
<div>
|
<div>
|
||||||
|
@ -81,7 +91,7 @@ const ModeToolsComponent = props => {
|
||||||
alt={props.intl.formatMessage(messages.brushSize)}
|
alt={props.intl.formatMessage(messages.brushSize)}
|
||||||
className={styles.modeToolsIcon}
|
className={styles.modeToolsIcon}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
src={brushIcon}
|
src={currentBrushIcon}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<LiveInput
|
<LiveInput
|
||||||
|
@ -90,11 +100,12 @@ const ModeToolsComponent = props => {
|
||||||
max={MAX_STROKE_WIDTH}
|
max={MAX_STROKE_WIDTH}
|
||||||
min="1"
|
min="1"
|
||||||
type="number"
|
type="number"
|
||||||
value={props.brushValue}
|
value={currentBrushValue}
|
||||||
onSubmit={props.onBrushSliderChange}
|
onSubmit={changeFunction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case Modes.ERASER:
|
case Modes.ERASER:
|
||||||
return (
|
return (
|
||||||
<div className={classNames(props.className, styles.modeTools)}>
|
<div className={classNames(props.className, styles.modeTools)}>
|
||||||
|
@ -174,15 +185,18 @@ const ModeToolsComponent = props => {
|
||||||
};
|
};
|
||||||
|
|
||||||
ModeToolsComponent.propTypes = {
|
ModeToolsComponent.propTypes = {
|
||||||
|
bitBrushSize: PropTypes.number,
|
||||||
brushValue: PropTypes.number,
|
brushValue: PropTypes.number,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
clipboardItems: PropTypes.arrayOf(PropTypes.array),
|
clipboardItems: PropTypes.arrayOf(PropTypes.array),
|
||||||
eraserValue: PropTypes.number,
|
eraserValue: PropTypes.number,
|
||||||
|
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
||||||
hasSelectedUncurvedPoints: PropTypes.bool,
|
hasSelectedUncurvedPoints: PropTypes.bool,
|
||||||
hasSelectedUnpointedPoints: PropTypes.bool,
|
hasSelectedUnpointedPoints: PropTypes.bool,
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
mode: PropTypes.string.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
onBrushSliderChange: PropTypes.func,
|
onBitBrushSliderChange: PropTypes.func.isRequired,
|
||||||
|
onBrushSliderChange: PropTypes.func.isRequired,
|
||||||
onCopyToClipboard: PropTypes.func.isRequired,
|
onCopyToClipboard: PropTypes.func.isRequired,
|
||||||
onCurvePoints: PropTypes.func.isRequired,
|
onCurvePoints: PropTypes.func.isRequired,
|
||||||
onEraserSliderChange: PropTypes.func,
|
onEraserSliderChange: PropTypes.func,
|
||||||
|
@ -195,6 +209,8 @@ ModeToolsComponent.propTypes = {
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
mode: state.scratchPaint.mode,
|
mode: state.scratchPaint.mode,
|
||||||
|
format: state.scratchPaint.format,
|
||||||
|
bitBrushSize: state.scratchPaint.bitBrushSize,
|
||||||
brushValue: state.scratchPaint.brushMode.brushSize,
|
brushValue: state.scratchPaint.brushMode.brushSize,
|
||||||
clipboardItems: state.scratchPaint.clipboard.items,
|
clipboardItems: state.scratchPaint.clipboard.items,
|
||||||
eraserValue: state.scratchPaint.eraserMode.brushSize,
|
eraserValue: state.scratchPaint.eraserMode.brushSize,
|
||||||
|
@ -204,6 +220,9 @@ const mapDispatchToProps = dispatch => ({
|
||||||
onBrushSliderChange: brushSize => {
|
onBrushSliderChange: brushSize => {
|
||||||
dispatch(changeBrushSize(brushSize));
|
dispatch(changeBrushSize(brushSize));
|
||||||
},
|
},
|
||||||
|
onBitBrushSliderChange: bitBrushSize => {
|
||||||
|
dispatch(changeBitBrushSize(bitBrushSize));
|
||||||
|
},
|
||||||
onEraserSliderChange: eraserSize => {
|
onEraserSliderChange: eraserSize => {
|
||||||
dispatch(changeEraserSize(eraserSize));
|
dispatch(changeEraserSize(eraserSize));
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import PaperCanvas from '../../containers/paper-canvas.jsx';
|
||||||
import {shouldShowGroup, shouldShowUngroup} from '../../helper/group';
|
import {shouldShowGroup, shouldShowUngroup} from '../../helper/group';
|
||||||
import {shouldShowBringForward, shouldShowSendBackward} from '../../helper/order';
|
import {shouldShowBringForward, shouldShowSendBackward} from '../../helper/order';
|
||||||
|
|
||||||
|
import BitBrushMode from '../../containers/bit-brush-mode.jsx';
|
||||||
import Box from '../box/box.jsx';
|
import Box from '../box/box.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';
|
||||||
|
@ -35,7 +36,7 @@ import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicat
|
||||||
import TextMode from '../../containers/text-mode.jsx';
|
import TextMode from '../../containers/text-mode.jsx';
|
||||||
|
|
||||||
import Formats from '../../lib/format';
|
import Formats from '../../lib/format';
|
||||||
import {isVector} from '../../lib/format';
|
import {isBitmap, isVector} from '../../lib/format';
|
||||||
import layout from '../../lib/layout-constants';
|
import layout from '../../lib/layout-constants';
|
||||||
import styles from './paint-editor.css';
|
import styles from './paint-editor.css';
|
||||||
|
|
||||||
|
@ -309,34 +310,56 @@ const PaintEditorComponent = props => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Second Row */}
|
{/* Second Row */}
|
||||||
<div className={styles.row}>
|
{isVector(props.format) ?
|
||||||
<InputGroup
|
<div className={styles.row}>
|
||||||
className={classNames(
|
<InputGroup
|
||||||
styles.row,
|
className={classNames(
|
||||||
styles.modDashedBorder,
|
styles.row,
|
||||||
styles.modLabeledIconHeight
|
styles.modDashedBorder,
|
||||||
)}
|
styles.modLabeledIconHeight
|
||||||
>
|
)}
|
||||||
{/* fill */}
|
>
|
||||||
<FillColorIndicatorComponent
|
{/* fill */}
|
||||||
className={styles.modMarginRight}
|
<FillColorIndicatorComponent
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
className={styles.modMarginRight}
|
||||||
/>
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
{/* stroke */}
|
/>
|
||||||
<StrokeColorIndicatorComponent
|
{/* stroke */}
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
<StrokeColorIndicatorComponent
|
||||||
/>
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
{/* stroke width */}
|
/>
|
||||||
<StrokeWidthIndicatorComponent
|
{/* stroke width */}
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
<StrokeWidthIndicatorComponent
|
||||||
/>
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
</InputGroup>
|
/>
|
||||||
<InputGroup className={styles.modModeTools}>
|
</InputGroup>
|
||||||
<ModeToolsContainer
|
<InputGroup className={styles.modModeTools}>
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
<ModeToolsContainer
|
||||||
/>
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
</InputGroup>
|
/>
|
||||||
</div>
|
</InputGroup>
|
||||||
|
</div> :
|
||||||
|
<div className={styles.row}>
|
||||||
|
<InputGroup
|
||||||
|
className={classNames(
|
||||||
|
styles.row,
|
||||||
|
styles.modDashedBorder,
|
||||||
|
styles.modLabeledIconHeight
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* fill */}
|
||||||
|
<FillColorIndicatorComponent
|
||||||
|
className={styles.modMarginRight}
|
||||||
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup className={styles.modModeTools}>
|
||||||
|
<ModeToolsContainer
|
||||||
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -375,6 +398,14 @@ const PaintEditorComponent = props => {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{props.canvas !== null ? ( // eslint-disable-line no-negated-condition
|
||||||
|
<div className={isBitmap(props.format) ? styles.modeSelector : styles.hidden}>
|
||||||
|
<BitBrushMode
|
||||||
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{/* Canvas */}
|
{/* Canvas */}
|
||||||
<div
|
<div
|
||||||
|
|
107
src/containers/bit-brush-mode.jsx
Normal file
107
src/containers/bit-brush-mode.jsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
|
import Modes from '../lib/modes';
|
||||||
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
|
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
|
||||||
|
import {changeMode} from '../reducers/modes';
|
||||||
|
import {clearSelectedItems} from '../reducers/selected-items';
|
||||||
|
import {clearSelection} from '../helper/selection';
|
||||||
|
|
||||||
|
import BitBrushModeComponent from '../components/bit-brush-mode/bit-brush-mode.jsx';
|
||||||
|
import BitBrushTool from '../helper/bit-tools/brush-tool';
|
||||||
|
|
||||||
|
class BitBrushMode extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'activateTool',
|
||||||
|
'deactivateTool'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
componentDidMount () {
|
||||||
|
if (this.props.isBitBrushModeActive) {
|
||||||
|
this.activateTool(this.props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (this.tool && nextProps.color !== this.props.color) {
|
||||||
|
this.tool.setColor(nextProps.color);
|
||||||
|
}
|
||||||
|
if (this.tool && nextProps.bitBrushSize !== this.props.bitBrushSize) {
|
||||||
|
this.tool.setBrushSize(nextProps.bitBrushSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextProps.isBitBrushModeActive && !this.props.isBitBrushModeActive) {
|
||||||
|
this.activateTool();
|
||||||
|
} else if (!nextProps.isBitBrushModeActive && this.props.isBitBrushModeActive) {
|
||||||
|
this.deactivateTool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shouldComponentUpdate (nextProps) {
|
||||||
|
return nextProps.isBitBrushModeActive !== this.props.isBitBrushModeActive;
|
||||||
|
}
|
||||||
|
activateTool () {
|
||||||
|
clearSelection(this.props.clearSelectedItems);
|
||||||
|
// Force the default brush color if fill is MIXED or transparent
|
||||||
|
let color = this.props.color;
|
||||||
|
if (!color || color === MIXED) {
|
||||||
|
this.props.onChangeFillColor(DEFAULT_COLOR);
|
||||||
|
color = DEFAULT_COLOR;
|
||||||
|
}
|
||||||
|
this.tool = new BitBrushTool(
|
||||||
|
this.props.onUpdateSvg
|
||||||
|
);
|
||||||
|
this.tool.setColor(color);
|
||||||
|
this.tool.setBrushSize(this.props.bitBrushSize);
|
||||||
|
|
||||||
|
this.tool.activate();
|
||||||
|
}
|
||||||
|
deactivateTool () {
|
||||||
|
this.tool.deactivateTool();
|
||||||
|
this.tool.remove();
|
||||||
|
this.tool = null;
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<BitBrushModeComponent
|
||||||
|
isSelected={this.props.isBitBrushModeActive}
|
||||||
|
onMouseDown={this.props.handleMouseDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BitBrushMode.propTypes = {
|
||||||
|
bitBrushSize: PropTypes.number.isRequired,
|
||||||
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
|
color: PropTypes.string,
|
||||||
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
|
isBitBrushModeActive: PropTypes.bool.isRequired,
|
||||||
|
onChangeFillColor: PropTypes.func.isRequired,
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
bitBrushSize: state.scratchPaint.bitBrushSize,
|
||||||
|
color: state.scratchPaint.color.fillColor,
|
||||||
|
isBitBrushModeActive: state.scratchPaint.mode === Modes.BIT_BRUSH
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
clearSelectedItems: () => {
|
||||||
|
dispatch(clearSelectedItems());
|
||||||
|
},
|
||||||
|
handleMouseDown: () => {
|
||||||
|
dispatch(changeMode(Modes.BIT_BRUSH));
|
||||||
|
},
|
||||||
|
onChangeFillColor: fillColor => {
|
||||||
|
dispatch(changeFillColor(fillColor));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BitBrushMode);
|
|
@ -7,7 +7,6 @@ import Blobbiness from '../helper/blob-tools/blob';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
|
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
|
||||||
import {changeBrushSize} from '../reducers/brush-mode';
|
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems} from '../reducers/selected-items';
|
||||||
import {clearSelection} from '../helper/selection';
|
import {clearSelection} from '../helper/selection';
|
||||||
|
@ -98,9 +97,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
changeBrushSize: brushSize => {
|
|
||||||
dispatch(changeBrushSize(brushSize));
|
|
||||||
},
|
|
||||||
handleMouseDown: () => {
|
handleMouseDown: () => {
|
||||||
dispatch(changeMode(Modes.BRUSH));
|
dispatch(changeMode(Modes.BRUSH));
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,7 +23,7 @@ import EyeDropperTool from '../helper/tools/eye-dropper';
|
||||||
|
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
import Formats from '../lib/format';
|
import Formats from '../lib/format';
|
||||||
import {isBitmap} from '../lib/format';
|
import {isBitmap, isVector} from '../lib/format';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
|
|
||||||
|
@ -79,6 +79,13 @@ class PaintEditor extends React.Component {
|
||||||
} else if (!this.props.isEyeDropping && prevProps.isEyeDropping) {
|
} else if (!this.props.isEyeDropping && prevProps.isEyeDropping) {
|
||||||
this.stopEyeDroppingLoop();
|
this.stopEyeDroppingLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @todo move to correct corresponding tool
|
||||||
|
if (isVector(this.props.format) && isBitmap(prevProps.format)) {
|
||||||
|
this.props.changeMode(Modes.BRUSH);
|
||||||
|
} else if (isVector(prevProps.format) && isBitmap(this.props.format)) {
|
||||||
|
this.props.changeMode(Modes.BIT_BRUSH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
document.removeEventListener('keydown', this.props.onKeyPress);
|
document.removeEventListener('keydown', this.props.onKeyPress);
|
||||||
|
@ -280,6 +287,7 @@ class PaintEditor extends React.Component {
|
||||||
|
|
||||||
PaintEditor.propTypes = {
|
PaintEditor.propTypes = {
|
||||||
changeColorToEyeDropper: PropTypes.func,
|
changeColorToEyeDropper: PropTypes.func,
|
||||||
|
changeMode: PropTypes.func.isRequired,
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
||||||
handleSwitchToBitmap: PropTypes.func.isRequired,
|
handleSwitchToBitmap: PropTypes.func.isRequired,
|
||||||
|
@ -344,6 +352,9 @@ const mapDispatchToProps = dispatch => ({
|
||||||
dispatch(changeMode(Modes.RECT));
|
dispatch(changeMode(Modes.RECT));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
changeMode: mode => {
|
||||||
|
dispatch(changeMode(mode));
|
||||||
|
},
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
|
|
128
src/helper/bit-tools/brush-tool.js
Normal file
128
src/helper/bit-tools/brush-tool.js
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import paper from '@scratch/paper';
|
||||||
|
import {getRaster} from '../layer';
|
||||||
|
import {forEachLinePoint, fillEllipse} from '../bitmap';
|
||||||
|
import {getGuideLayer} from '../layer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool for drawing with the bitmap brush.
|
||||||
|
*/
|
||||||
|
class BrushTool extends paper.Tool {
|
||||||
|
/**
|
||||||
|
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||||
|
*/
|
||||||
|
constructor (onUpdateSvg) {
|
||||||
|
super();
|
||||||
|
this.onUpdateSvg = onUpdateSvg;
|
||||||
|
|
||||||
|
// We have to set these functions instead of just declaring them because
|
||||||
|
// paper.js tools hook up the listeners in the setter functions.
|
||||||
|
this.onMouseMove = this.handleMouseMove;
|
||||||
|
this.onMouseDown = this.handleMouseDown;
|
||||||
|
this.onMouseDrag = this.handleMouseDrag;
|
||||||
|
this.onMouseUp = this.handleMouseUp;
|
||||||
|
|
||||||
|
this.colorState = null;
|
||||||
|
this.active = false;
|
||||||
|
this.lastPoint = null;
|
||||||
|
this.cursorPreview = null;
|
||||||
|
}
|
||||||
|
setColor (color) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
setBrushSize (size) {
|
||||||
|
// For performance, make sure this is an integer
|
||||||
|
this.size = Math.max(1, ~~size);
|
||||||
|
}
|
||||||
|
// Draw a brush mark at the given point
|
||||||
|
draw (x, y) {
|
||||||
|
const roundedUpRadius = Math.ceil(this.size / 2);
|
||||||
|
getRaster().drawImage(this.tmpCanvas, new paper.Point(~~x - roundedUpRadius, ~~y - roundedUpRadius));
|
||||||
|
}
|
||||||
|
updateCursorIfNeeded () {
|
||||||
|
if (!this.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The cursor preview was unattached from the view by an outside process,
|
||||||
|
// such as changing costumes or undo.
|
||||||
|
if (this.cursorPreview && !this.cursorPreview.parent) {
|
||||||
|
this.cursorPreview = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.cursorPreview || !(this.lastSize === this.size && this.lastColor === this.color)) {
|
||||||
|
if (this.cursorPreview) {
|
||||||
|
this.cursorPreview.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tmpCanvas = document.createElement('canvas');
|
||||||
|
const roundedUpRadius = Math.ceil(this.size / 2);
|
||||||
|
this.tmpCanvas.width = roundedUpRadius * 2;
|
||||||
|
this.tmpCanvas.height = roundedUpRadius * 2;
|
||||||
|
const context = this.tmpCanvas.getContext('2d');
|
||||||
|
context.imageSmoothingEnabled = false;
|
||||||
|
context.fillStyle = this.color;
|
||||||
|
// Small squares for pixel artists
|
||||||
|
if (this.size <= 5) {
|
||||||
|
if (this.size % 2) {
|
||||||
|
context.fillRect(1, 1, this.size, this.size);
|
||||||
|
} else {
|
||||||
|
context.fillRect(0, 0, this.size, this.size);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const roundedDownRadius = ~~(this.size / 2);
|
||||||
|
fillEllipse(roundedDownRadius, roundedDownRadius, roundedDownRadius, roundedDownRadius, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursorPreview = new paper.Raster(this.tmpCanvas);
|
||||||
|
this.cursorPreview.guide = true;
|
||||||
|
this.cursorPreview.parent = getGuideLayer();
|
||||||
|
this.cursorPreview.data.isHelperItem = true;
|
||||||
|
}
|
||||||
|
this.lastSize = this.size;
|
||||||
|
this.lastColor = this.color;
|
||||||
|
}
|
||||||
|
handleMouseMove (event) {
|
||||||
|
this.updateCursorIfNeeded();
|
||||||
|
this.cursorPreview.position = new paper.Point(~~event.point.x, ~~event.point.y);
|
||||||
|
}
|
||||||
|
handleMouseDown (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
this.active = true;
|
||||||
|
|
||||||
|
this.cursorPreview.remove();
|
||||||
|
|
||||||
|
this.draw(event.point.x, event.point.y);
|
||||||
|
this.lastPoint = event.point;
|
||||||
|
}
|
||||||
|
handleMouseDrag (event) {
|
||||||
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
|
||||||
|
if (this.isBoundingBoxMode) {
|
||||||
|
this.boundingBoxTool.onMouseDrag(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
forEachLinePoint(this.lastPoint, event.point, this.draw.bind(this));
|
||||||
|
this.lastPoint = event.point;
|
||||||
|
}
|
||||||
|
handleMouseUp (event) {
|
||||||
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
|
||||||
|
forEachLinePoint(this.lastPoint, event.point, this.draw.bind(this));
|
||||||
|
this.onUpdateSvg();
|
||||||
|
|
||||||
|
this.lastPoint = null;
|
||||||
|
this.active = false;
|
||||||
|
|
||||||
|
this.updateCursorIfNeeded();
|
||||||
|
this.cursorPreview.position = new paper.Point(~~event.point.x, ~~event.point.y);
|
||||||
|
}
|
||||||
|
deactivateTool () {
|
||||||
|
this.active = false;
|
||||||
|
this.tmpCanvas = null;
|
||||||
|
if (this.cursorPreview) {
|
||||||
|
this.cursorPreview.remove();
|
||||||
|
this.cursorPreview = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BrushTool;
|
|
@ -1,5 +1,86 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
|
|
||||||
|
const forEachLinePoint = function (point1, point2, callback) {
|
||||||
|
// Bresenham line algorithm
|
||||||
|
let x1 = ~~point1.x;
|
||||||
|
const x2 = ~~point2.x;
|
||||||
|
let y1 = ~~point1.y;
|
||||||
|
const y2 = ~~point2.y;
|
||||||
|
|
||||||
|
const dx = Math.abs(x2 - x1);
|
||||||
|
const dy = Math.abs(y2 - y1);
|
||||||
|
const sx = (x1 < x2) ? 1 : -1;
|
||||||
|
const sy = (y1 < y2) ? 1 : -1;
|
||||||
|
let err = dx - dy;
|
||||||
|
|
||||||
|
callback(x1, y1);
|
||||||
|
while (x1 !== x2 || y1 !== y2) {
|
||||||
|
const e2 = err * 2;
|
||||||
|
if (e2 > -dy) {
|
||||||
|
err -= dy;
|
||||||
|
x1 += sx;
|
||||||
|
}
|
||||||
|
if (e2 < dx) {
|
||||||
|
err += dx;
|
||||||
|
y1 += sy;
|
||||||
|
}
|
||||||
|
callback(x1, y1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fillEllipse = function (centerX, centerY, radiusX, radiusY, context) {
|
||||||
|
// Bresenham ellipse algorithm
|
||||||
|
centerX = ~~centerX;
|
||||||
|
centerY = ~~centerY;
|
||||||
|
radiusX = ~~radiusX;
|
||||||
|
radiusY = ~~radiusY;
|
||||||
|
const twoRadXSquared = 2 * radiusX * radiusX;
|
||||||
|
const twoRadYSquared = 2 * radiusY * radiusY;
|
||||||
|
let x = radiusX;
|
||||||
|
let y = 0;
|
||||||
|
let dx = radiusY * radiusY * (1 - (radiusX << 1));
|
||||||
|
let dy = radiusX * radiusX;
|
||||||
|
let error = 0;
|
||||||
|
let stoppingX = twoRadYSquared * radiusX;
|
||||||
|
let stoppingY = 0;
|
||||||
|
|
||||||
|
while (stoppingX >= stoppingY) {
|
||||||
|
context.fillRect(centerX - x, centerY - y, x << 1, y << 1);
|
||||||
|
y++;
|
||||||
|
stoppingY += twoRadXSquared;
|
||||||
|
error += dy;
|
||||||
|
dy += twoRadXSquared;
|
||||||
|
if ((error << 1) + dx > 0) {
|
||||||
|
x--;
|
||||||
|
stoppingX -= twoRadYSquared;
|
||||||
|
error += dx;
|
||||||
|
dx += twoRadYSquared;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x = 0;
|
||||||
|
y = radiusY;
|
||||||
|
dx = radiusY * radiusY;
|
||||||
|
dy = radiusX * radiusX * (1 - (radiusY << 1));
|
||||||
|
error = 0;
|
||||||
|
stoppingX = 0;
|
||||||
|
stoppingY = twoRadXSquared * radiusY;
|
||||||
|
while (stoppingX <= stoppingY) {
|
||||||
|
context.fillRect(centerX - x, centerY - y, x * 2, y * 2);
|
||||||
|
x++;
|
||||||
|
stoppingX += twoRadYSquared;
|
||||||
|
error += dx;
|
||||||
|
dx += twoRadYSquared;
|
||||||
|
if ((error << 1) + dy > 0) {
|
||||||
|
y--;
|
||||||
|
stoppingY -= twoRadXSquared;
|
||||||
|
error += dy;
|
||||||
|
dy += twoRadXSquared;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const rowBlank_ = function (imageData, width, y) {
|
const rowBlank_ = function (imageData, width, y) {
|
||||||
for (let x = 0; x < width; ++x) {
|
for (let x = 0; x < width; ++x) {
|
||||||
if (imageData.data[(y * width << 2) + (x << 2) + 3] !== 0) return false;
|
if (imageData.data[(y * width << 2) + (x << 2) + 3] !== 0) return false;
|
||||||
|
@ -33,5 +114,7 @@ const trim = function (raster) {
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
fillEllipse,
|
||||||
|
forEachLinePoint,
|
||||||
trim
|
trim
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,15 @@ const clearRaster = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRaster = function () {
|
const getRaster = function () {
|
||||||
|
const layer = _getLayer('isRasterLayer');
|
||||||
|
// Generate blank raster
|
||||||
|
if (layer.children.length === 0) {
|
||||||
|
const raster = new paper.Raster(rasterSrc);
|
||||||
|
raster.parent = layer;
|
||||||
|
raster.guide = true;
|
||||||
|
raster.locked = true;
|
||||||
|
raster.position = paper.view.center;
|
||||||
|
}
|
||||||
return _getLayer('isRasterLayer').children[0];
|
return _getLayer('isRasterLayer').children[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import keyMirror from 'keymirror';
|
import keyMirror from 'keymirror';
|
||||||
|
|
||||||
const Modes = keyMirror({
|
const Modes = keyMirror({
|
||||||
|
BIT_BRUSH: null,
|
||||||
BRUSH: null,
|
BRUSH: null,
|
||||||
ERASER: null,
|
ERASER: null,
|
||||||
LINE: null,
|
LINE: null,
|
||||||
|
|
33
src/reducers/bit-brush-size.js
Normal file
33
src/reducers/bit-brush-size.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import log from '../log/log';
|
||||||
|
|
||||||
|
// Bit brush size affects bit brush width, circle/rectangle outline drawing width, and line width
|
||||||
|
// in the bitmap paint editor.
|
||||||
|
const CHANGE_BIT_BRUSH_SIZE = 'scratch-paint/brush-mode/CHANGE_BIT_BRUSH_SIZE';
|
||||||
|
const initialState = 10;
|
||||||
|
|
||||||
|
const reducer = function (state, action) {
|
||||||
|
if (typeof state === 'undefined') state = initialState;
|
||||||
|
switch (action.type) {
|
||||||
|
case CHANGE_BIT_BRUSH_SIZE:
|
||||||
|
if (isNaN(action.brushSize)) {
|
||||||
|
log.warn(`Invalid brush size: ${action.brushSize}`);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return Math.max(1, action.brushSize);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action creators ==================================
|
||||||
|
const changeBitBrushSize = function (brushSize) {
|
||||||
|
return {
|
||||||
|
type: CHANGE_BIT_BRUSH_SIZE,
|
||||||
|
brushSize: brushSize
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
reducer as default,
|
||||||
|
changeBitBrushSize
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
import {combineReducers} from 'redux';
|
import {combineReducers} from 'redux';
|
||||||
import modeReducer from './modes';
|
import modeReducer from './modes';
|
||||||
|
import bitBrushSizeReducer from './bit-brush-size';
|
||||||
import brushModeReducer from './brush-mode';
|
import brushModeReducer from './brush-mode';
|
||||||
import eraserModeReducer from './eraser-mode';
|
import eraserModeReducer from './eraser-mode';
|
||||||
import colorReducer from './color';
|
import colorReducer from './color';
|
||||||
|
@ -14,6 +15,7 @@ import undoReducer from './undo';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
mode: modeReducer,
|
mode: modeReducer,
|
||||||
|
bitBrushSize: bitBrushSizeReducer,
|
||||||
brushMode: brushModeReducer,
|
brushMode: brushModeReducer,
|
||||||
color: colorReducer,
|
color: colorReducer,
|
||||||
clipboard: clipboardReducer,
|
clipboard: clipboardReducer,
|
||||||
|
|
Loading…
Reference in a new issue