mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -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 as changeEraserSize} from '../../reducers/eraser-mode';
|
||||
import {changeBitBrushSize} from '../../reducers/bit-brush-size';
|
||||
|
||||
import LiveInputHOC from '../forms/live-input-hoc.jsx';
|
||||
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 LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
|
||||
import Modes from '../../lib/modes';
|
||||
import Formats from '../../lib/format';
|
||||
import {isBitmap} from '../../lib/format';
|
||||
import styles from './mode-tools.css';
|
||||
|
||||
import copyIcon from './icons/copy.svg';
|
||||
import pasteIcon from './icons/paste.svg';
|
||||
|
||||
import brushIcon from '../brush-mode/brush.svg';
|
||||
import bitBrushIcon from '../bit-brush-mode/brush.svg';
|
||||
import curvedPointIcon from './icons/curved-point.svg';
|
||||
import eraserIcon from '../eraser-mode/eraser.svg';
|
||||
import flipHorizontalIcon from './icons/flip-horizontal.svg';
|
||||
|
@ -74,6 +78,12 @@ const ModeToolsComponent = props => {
|
|||
|
||||
switch (props.mode) {
|
||||
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 (
|
||||
<div className={classNames(props.className, styles.modeTools)}>
|
||||
<div>
|
||||
|
@ -81,7 +91,7 @@ const ModeToolsComponent = props => {
|
|||
alt={props.intl.formatMessage(messages.brushSize)}
|
||||
className={styles.modeToolsIcon}
|
||||
draggable={false}
|
||||
src={brushIcon}
|
||||
src={currentBrushIcon}
|
||||
/>
|
||||
</div>
|
||||
<LiveInput
|
||||
|
@ -90,11 +100,12 @@ const ModeToolsComponent = props => {
|
|||
max={MAX_STROKE_WIDTH}
|
||||
min="1"
|
||||
type="number"
|
||||
value={props.brushValue}
|
||||
onSubmit={props.onBrushSliderChange}
|
||||
value={currentBrushValue}
|
||||
onSubmit={changeFunction}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case Modes.ERASER:
|
||||
return (
|
||||
<div className={classNames(props.className, styles.modeTools)}>
|
||||
|
@ -174,15 +185,18 @@ const ModeToolsComponent = props => {
|
|||
};
|
||||
|
||||
ModeToolsComponent.propTypes = {
|
||||
bitBrushSize: PropTypes.number,
|
||||
brushValue: PropTypes.number,
|
||||
className: PropTypes.string,
|
||||
clipboardItems: PropTypes.arrayOf(PropTypes.array),
|
||||
eraserValue: PropTypes.number,
|
||||
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
||||
hasSelectedUncurvedPoints: PropTypes.bool,
|
||||
hasSelectedUnpointedPoints: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
onBrushSliderChange: PropTypes.func,
|
||||
onBitBrushSliderChange: PropTypes.func.isRequired,
|
||||
onBrushSliderChange: PropTypes.func.isRequired,
|
||||
onCopyToClipboard: PropTypes.func.isRequired,
|
||||
onCurvePoints: PropTypes.func.isRequired,
|
||||
onEraserSliderChange: PropTypes.func,
|
||||
|
@ -195,6 +209,8 @@ ModeToolsComponent.propTypes = {
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
mode: state.scratchPaint.mode,
|
||||
format: state.scratchPaint.format,
|
||||
bitBrushSize: state.scratchPaint.bitBrushSize,
|
||||
brushValue: state.scratchPaint.brushMode.brushSize,
|
||||
clipboardItems: state.scratchPaint.clipboard.items,
|
||||
eraserValue: state.scratchPaint.eraserMode.brushSize,
|
||||
|
@ -204,6 +220,9 @@ const mapDispatchToProps = dispatch => ({
|
|||
onBrushSliderChange: brushSize => {
|
||||
dispatch(changeBrushSize(brushSize));
|
||||
},
|
||||
onBitBrushSliderChange: bitBrushSize => {
|
||||
dispatch(changeBitBrushSize(bitBrushSize));
|
||||
},
|
||||
onEraserSliderChange: eraserSize => {
|
||||
dispatch(changeEraserSize(eraserSize));
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import PaperCanvas from '../../containers/paper-canvas.jsx';
|
|||
import {shouldShowGroup, shouldShowUngroup} from '../../helper/group';
|
||||
import {shouldShowBringForward, shouldShowSendBackward} from '../../helper/order';
|
||||
|
||||
import BitBrushMode from '../../containers/bit-brush-mode.jsx';
|
||||
import Box from '../box/box.jsx';
|
||||
import Button from '../button/button.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 Formats from '../../lib/format';
|
||||
import {isVector} from '../../lib/format';
|
||||
import {isBitmap, isVector} from '../../lib/format';
|
||||
import layout from '../../lib/layout-constants';
|
||||
import styles from './paint-editor.css';
|
||||
|
||||
|
@ -309,34 +310,56 @@ const PaintEditorComponent = props => {
|
|||
</div>
|
||||
|
||||
{/* Second Row */}
|
||||
<div className={styles.row}>
|
||||
<InputGroup
|
||||
className={classNames(
|
||||
styles.row,
|
||||
styles.modDashedBorder,
|
||||
styles.modLabeledIconHeight
|
||||
)}
|
||||
>
|
||||
{/* fill */}
|
||||
<FillColorIndicatorComponent
|
||||
className={styles.modMarginRight}
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* stroke */}
|
||||
<StrokeColorIndicatorComponent
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* stroke width */}
|
||||
<StrokeWidthIndicatorComponent
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup className={styles.modModeTools}>
|
||||
<ModeToolsContainer
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
{isVector(props.format) ?
|
||||
<div className={styles.row}>
|
||||
<InputGroup
|
||||
className={classNames(
|
||||
styles.row,
|
||||
styles.modDashedBorder,
|
||||
styles.modLabeledIconHeight
|
||||
)}
|
||||
>
|
||||
{/* fill */}
|
||||
<FillColorIndicatorComponent
|
||||
className={styles.modMarginRight}
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* stroke */}
|
||||
<StrokeColorIndicatorComponent
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* stroke width */}
|
||||
<StrokeWidthIndicatorComponent
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup className={styles.modModeTools}>
|
||||
<ModeToolsContainer
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
</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>
|
||||
) : null}
|
||||
|
||||
|
@ -375,6 +398,14 @@ const PaintEditorComponent = props => {
|
|||
</div>
|
||||
) : 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>
|
||||
{/* Canvas */}
|
||||
<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 {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
|
||||
import {changeBrushSize} from '../reducers/brush-mode';
|
||||
import {changeMode} from '../reducers/modes';
|
||||
import {clearSelectedItems} from '../reducers/selected-items';
|
||||
import {clearSelection} from '../helper/selection';
|
||||
|
@ -98,9 +97,6 @@ const mapDispatchToProps = dispatch => ({
|
|||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
changeBrushSize: brushSize => {
|
||||
dispatch(changeBrushSize(brushSize));
|
||||
},
|
||||
handleMouseDown: () => {
|
||||
dispatch(changeMode(Modes.BRUSH));
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ import EyeDropperTool from '../helper/tools/eye-dropper';
|
|||
|
||||
import Modes from '../lib/modes';
|
||||
import Formats from '../lib/format';
|
||||
import {isBitmap} from '../lib/format';
|
||||
import {isBitmap, isVector} from '../lib/format';
|
||||
import {connect} from 'react-redux';
|
||||
import bindAll from 'lodash.bindall';
|
||||
|
||||
|
@ -79,6 +79,13 @@ class PaintEditor extends React.Component {
|
|||
} else if (!this.props.isEyeDropping && prevProps.isEyeDropping) {
|
||||
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 () {
|
||||
document.removeEventListener('keydown', this.props.onKeyPress);
|
||||
|
@ -280,6 +287,7 @@ class PaintEditor extends React.Component {
|
|||
|
||||
PaintEditor.propTypes = {
|
||||
changeColorToEyeDropper: PropTypes.func,
|
||||
changeMode: PropTypes.func.isRequired,
|
||||
clearSelectedItems: PropTypes.func.isRequired,
|
||||
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
||||
handleSwitchToBitmap: PropTypes.func.isRequired,
|
||||
|
@ -344,6 +352,9 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(changeMode(Modes.RECT));
|
||||
}
|
||||
},
|
||||
changeMode: mode => {
|
||||
dispatch(changeMode(mode));
|
||||
},
|
||||
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';
|
||||
|
||||
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) {
|
||||
for (let x = 0; x < width; ++x) {
|
||||
if (imageData.data[(y * width << 2) + (x << 2) + 3] !== 0) return false;
|
||||
|
@ -33,5 +114,7 @@ const trim = function (raster) {
|
|||
};
|
||||
|
||||
export {
|
||||
fillEllipse,
|
||||
forEachLinePoint,
|
||||
trim
|
||||
};
|
||||
|
|
|
@ -27,6 +27,15 @@ const clearRaster = 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];
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import keyMirror from 'keymirror';
|
||||
|
||||
const Modes = keyMirror({
|
||||
BIT_BRUSH: null,
|
||||
BRUSH: null,
|
||||
ERASER: 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 modeReducer from './modes';
|
||||
import bitBrushSizeReducer from './bit-brush-size';
|
||||
import brushModeReducer from './brush-mode';
|
||||
import eraserModeReducer from './eraser-mode';
|
||||
import colorReducer from './color';
|
||||
|
@ -14,6 +15,7 @@ import undoReducer from './undo';
|
|||
|
||||
export default combineReducers({
|
||||
mode: modeReducer,
|
||||
bitBrushSize: bitBrushSizeReducer,
|
||||
brushMode: brushModeReducer,
|
||||
color: colorReducer,
|
||||
clipboard: clipboardReducer,
|
||||
|
|
Loading…
Reference in a new issue