mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-23 05:52:42 -05:00
Make the text tool a real tool
This commit is contained in:
parent
0d3b276e5e
commit
8e7ae67ae6
5 changed files with 289 additions and 26 deletions
|
@ -33,7 +33,7 @@ import ReshapeMode from '../../containers/reshape-mode.jsx';
|
||||||
import SelectMode from '../../containers/select-mode.jsx';
|
import SelectMode from '../../containers/select-mode.jsx';
|
||||||
import StrokeColorIndicatorComponent from '../../containers/stroke-color-indicator.jsx';
|
import StrokeColorIndicatorComponent from '../../containers/stroke-color-indicator.jsx';
|
||||||
import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicator.jsx';
|
import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicator.jsx';
|
||||||
import TextModeComponent from '../text-mode/text-mode.jsx';
|
import TextMode from '../../containers/text-mode.jsx';
|
||||||
|
|
||||||
import layout from '../../lib/layout-constants';
|
import layout from '../../lib/layout-constants';
|
||||||
import styles from './paint-editor.css';
|
import styles from './paint-editor.css';
|
||||||
|
@ -350,11 +350,13 @@ const PaintEditorComponent = props => {
|
||||||
<EraserMode
|
<EraserMode
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
/>
|
/>
|
||||||
{/* Text mode will go here */}
|
<FillMode
|
||||||
<LineMode
|
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
/>
|
/>
|
||||||
<FillMode
|
<TextMode
|
||||||
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
|
/>
|
||||||
|
<LineMode
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
/>
|
/>
|
||||||
<OvalMode
|
<OvalMode
|
||||||
|
@ -363,8 +365,6 @@ const PaintEditorComponent = props => {
|
||||||
<RectMode
|
<RectMode
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
/>
|
/>
|
||||||
{/* text tool, coming soon */}
|
|
||||||
<TextModeComponent />
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,25 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
|
|
||||||
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
|
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
|
||||||
|
|
||||||
import textIcon from './text.svg';
|
import textIcon from './text.svg';
|
||||||
|
|
||||||
const TextModeComponent = () => (
|
const TextModeComponent = props => (
|
||||||
<ComingSoonTooltip
|
<ToolSelectComponent
|
||||||
place="right"
|
imgDescriptor={{
|
||||||
tooltipId="text-mode-select"
|
defaultMessage: 'Text',
|
||||||
>
|
description: 'Label for the text tool',
|
||||||
<ToolSelectComponent
|
id: 'paint.textMode.text'
|
||||||
disabled
|
}}
|
||||||
imgDescriptor={{
|
imgSrc={textIcon}
|
||||||
defaultMessage: 'Text',
|
isSelected={props.isSelected}
|
||||||
description: 'Label for the text tool',
|
onMouseDown={props.onMouseDown}
|
||||||
id: 'paint.textMode.text'
|
/>
|
||||||
}}
|
|
||||||
imgSrc={textIcon}
|
|
||||||
isSelected={false}
|
|
||||||
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
|
|
||||||
/>
|
|
||||||
</ComingSoonTooltip>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextModeComponent.propTypes = {
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
onMouseDown: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default TextModeComponent;
|
export default TextModeComponent;
|
||||||
|
|
130
src/containers/text-mode.jsx
Normal file
130
src/containers/text-mode.jsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import paper from '@scratch/paper';
|
||||||
|
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 {changeStrokeColor} from '../reducers/stroke-color';
|
||||||
|
import {changeMode} from '../reducers/modes';
|
||||||
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
|
|
||||||
|
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
|
||||||
|
import TextTool from '../helper/tools/text-tool';
|
||||||
|
import TextModeComponent from '../components/text-mode/text-mode.jsx';
|
||||||
|
|
||||||
|
class TextMode extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'activateTool',
|
||||||
|
'deactivateTool'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
componentDidMount () {
|
||||||
|
if (this.props.isTextModeActive) {
|
||||||
|
this.activateTool(this.props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (this.tool && nextProps.colorState !== this.props.colorState) {
|
||||||
|
this.tool.setColorState(nextProps.colorState);
|
||||||
|
}
|
||||||
|
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
|
||||||
|
this.tool.onSelectionChanged(nextProps.selectedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextProps.isTextModeActive && !this.props.isTextModeActive) {
|
||||||
|
this.activateTool();
|
||||||
|
} else if (!nextProps.isTextModeActive && this.props.isTextModeActive) {
|
||||||
|
this.deactivateTool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shouldComponentUpdate (nextProps) {
|
||||||
|
return nextProps.isTextModeActive !== this.props.isTextModeActive;
|
||||||
|
}
|
||||||
|
activateTool () {
|
||||||
|
clearSelection(this.props.clearSelectedItems);
|
||||||
|
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
||||||
|
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
||||||
|
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
||||||
|
const {fillColor, strokeColor, strokeWidth} = this.props.colorState;
|
||||||
|
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
||||||
|
const strokeColorPresent =
|
||||||
|
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
||||||
|
if (!fillColorPresent && !strokeColorPresent) {
|
||||||
|
this.props.onChangeFillColor(DEFAULT_COLOR);
|
||||||
|
this.props.onChangeStrokeColor(null);
|
||||||
|
} else if (!fillColorPresent && strokeColorPresent) {
|
||||||
|
this.props.onChangeFillColor(null);
|
||||||
|
} else if (fillColorPresent && !strokeColorPresent) {
|
||||||
|
this.props.onChangeStrokeColor(null);
|
||||||
|
}
|
||||||
|
this.tool = new TextTool(
|
||||||
|
this.props.setSelectedItems,
|
||||||
|
this.props.clearSelectedItems,
|
||||||
|
this.props.onUpdateSvg
|
||||||
|
);
|
||||||
|
this.tool.setColorState(this.props.colorState);
|
||||||
|
this.tool.activate();
|
||||||
|
}
|
||||||
|
deactivateTool () {
|
||||||
|
this.tool.deactivateTool();
|
||||||
|
this.tool.remove();
|
||||||
|
this.tool = null;
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<TextModeComponent
|
||||||
|
isSelected={this.props.isTextModeActive}
|
||||||
|
onMouseDown={this.props.handleMouseDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMode.propTypes = {
|
||||||
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
|
colorState: PropTypes.shape({
|
||||||
|
fillColor: PropTypes.string,
|
||||||
|
strokeColor: PropTypes.string,
|
||||||
|
strokeWidth: PropTypes.number
|
||||||
|
}).isRequired,
|
||||||
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
|
isTextModeActive: PropTypes.bool.isRequired,
|
||||||
|
onChangeFillColor: PropTypes.func.isRequired,
|
||||||
|
onChangeStrokeColor: PropTypes.func.isRequired,
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
|
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)),
|
||||||
|
setSelectedItems: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
colorState: state.scratchPaint.color,
|
||||||
|
isTextModeActive: state.scratchPaint.mode === Modes.TEXT,
|
||||||
|
selectedItems: state.scratchPaint.selectedItems
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
clearSelectedItems: () => {
|
||||||
|
dispatch(clearSelectedItems());
|
||||||
|
},
|
||||||
|
setSelectedItems: () => {
|
||||||
|
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||||
|
},
|
||||||
|
handleMouseDown: () => {
|
||||||
|
dispatch(changeMode(Modes.TEXT));
|
||||||
|
},
|
||||||
|
onChangeFillColor: fillColor => {
|
||||||
|
dispatch(changeFillColor(fillColor));
|
||||||
|
},
|
||||||
|
onChangeStrokeColor: strokeColor => {
|
||||||
|
dispatch(changeStrokeColor(strokeColor));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TextMode);
|
134
src/helper/tools/text-tool.js
Normal file
134
src/helper/tools/text-tool.js
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import paper from '@scratch/paper';
|
||||||
|
import Modes from '../../lib/modes';
|
||||||
|
import {styleShape} from '../style-path';
|
||||||
|
import {clearSelection} from '../selection';
|
||||||
|
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||||
|
import NudgeTool from '../selection-tools/nudge-tool';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool for adding text. Text elements have limited editability; they can't be reshaped,
|
||||||
|
* drawn on or erased. This way they can preserve their ability to have the text edited.
|
||||||
|
*/
|
||||||
|
class TextTool extends paper.Tool {
|
||||||
|
static get TOLERANCE () {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
|
||||||
|
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
||||||
|
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||||
|
*/
|
||||||
|
constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) {
|
||||||
|
super();
|
||||||
|
this.setSelectedItems = setSelectedItems;
|
||||||
|
this.clearSelectedItems = clearSelectedItems;
|
||||||
|
this.onUpdateSvg = onUpdateSvg;
|
||||||
|
this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateSvg);
|
||||||
|
const nudgeTool = new NudgeTool(this.boundingBoxTool, 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.onMouseDown = this.handleMouseDown;
|
||||||
|
this.onMouseDrag = this.handleMouseDrag;
|
||||||
|
this.onMouseUp = this.handleMouseUp;
|
||||||
|
this.onKeyUp = nudgeTool.onKeyUp;
|
||||||
|
this.onKeyDown = nudgeTool.onKeyDown;
|
||||||
|
|
||||||
|
this.oval = null;
|
||||||
|
this.colorState = null;
|
||||||
|
this.isBoundingBoxMode = null;
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
getHitOptions () {
|
||||||
|
return {
|
||||||
|
segments: true,
|
||||||
|
stroke: true,
|
||||||
|
curves: true,
|
||||||
|
fill: true,
|
||||||
|
guide: false,
|
||||||
|
match: hitResult =>
|
||||||
|
(hitResult.item.data && hitResult.item.data.isHelperItem) ||
|
||||||
|
hitResult.item.selected, // Allow hits on bounding box and selected only
|
||||||
|
tolerance: TextTool.TOLERANCE / paper.view.zoom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Should be called if the selection changes to update the bounds of the bounding box.
|
||||||
|
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
||||||
|
*/
|
||||||
|
onSelectionChanged (selectedItems) {
|
||||||
|
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
||||||
|
}
|
||||||
|
setColorState (colorState) {
|
||||||
|
this.colorState = colorState;
|
||||||
|
}
|
||||||
|
handleMouseDown (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
this.active = true;
|
||||||
|
|
||||||
|
if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) {
|
||||||
|
this.isBoundingBoxMode = true;
|
||||||
|
} else {
|
||||||
|
this.isBoundingBoxMode = false;
|
||||||
|
clearSelection(this.clearSelectedItems);
|
||||||
|
this.oval = new paper.Shape.Ellipse({
|
||||||
|
point: event.downPoint,
|
||||||
|
size: 0
|
||||||
|
});
|
||||||
|
styleShape(this.oval, this.colorState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleMouseDrag (event) {
|
||||||
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
|
||||||
|
if (this.isBoundingBoxMode) {
|
||||||
|
this.boundingBoxTool.onMouseDrag(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const downPoint = new paper.Point(event.downPoint.x, event.downPoint.y);
|
||||||
|
const point = new paper.Point(event.point.x, event.point.y);
|
||||||
|
if (event.modifiers.shift) {
|
||||||
|
this.oval.size = new paper.Point(event.downPoint.x - event.point.x, event.downPoint.x - event.point.x);
|
||||||
|
} else {
|
||||||
|
this.oval.size = downPoint.subtract(point);
|
||||||
|
}
|
||||||
|
if (event.modifiers.alt) {
|
||||||
|
this.oval.position = downPoint;
|
||||||
|
} else {
|
||||||
|
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
handleMouseUp (event) {
|
||||||
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
|
||||||
|
if (this.isBoundingBoxMode) {
|
||||||
|
this.boundingBoxTool.onMouseUp(event);
|
||||||
|
this.isBoundingBoxMode = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.oval) {
|
||||||
|
if (Math.abs(this.oval.size.width * this.oval.size.height) < TextTool.TOLERANCE / paper.view.zoom) {
|
||||||
|
// Tiny oval created unintentionally?
|
||||||
|
this.oval.remove();
|
||||||
|
this.oval = null;
|
||||||
|
} else {
|
||||||
|
const ovalPath = this.oval.toPath(true /* insert */);
|
||||||
|
this.oval.remove();
|
||||||
|
this.oval = null;
|
||||||
|
|
||||||
|
ovalPath.selected = true;
|
||||||
|
this.setSelectedItems();
|
||||||
|
this.onUpdateSvg();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
deactivateTool () {
|
||||||
|
this.boundingBoxTool.removeBoundsPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextTool;
|
|
@ -9,7 +9,8 @@ const Modes = keyMirror({
|
||||||
RESHAPE: null,
|
RESHAPE: null,
|
||||||
OVAL: null,
|
OVAL: null,
|
||||||
RECT: null,
|
RECT: null,
|
||||||
ROUNDED_RECT: null
|
ROUNDED_RECT: null,
|
||||||
|
TEXT: null
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Modes;
|
export default Modes;
|
||||||
|
|
Loading…
Reference in a new issue