Bit text tool (#515)

This commit is contained in:
DD Liu 2018-06-19 14:54:29 -04:00 committed by GitHub
parent a70f8e1f36
commit d7298c0c43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 142 additions and 86 deletions

View file

@ -1,27 +1,26 @@
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 textIcon from './text.svg';
const BitTextComponent = () => (
<ComingSoonTooltip
place="right"
tooltipId="bit-text-mode"
>
<ToolSelectComponent
disabled
imgDescriptor={{
defaultMessage: 'Text',
description: 'Label for the text tool',
id: 'paint.textMode.text'
}}
imgSrc={textIcon}
isSelected={false}
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
/>
</ComingSoonTooltip>
const BitTextComponent = props => (
<ToolSelectComponent
imgDescriptor={{
defaultMessage: 'Text',
description: 'Label for the text tool',
id: 'paint.textMode.text'
}}
imgSrc={textIcon}
isSelected={props.isSelected}
onMouseDown={props.onMouseDown}
/>
);
BitTextComponent.propTypes = {
isSelected: PropTypes.bool.isRequired,
onMouseDown: PropTypes.func.isRequired
};
export default BitTextComponent;

View file

@ -196,6 +196,8 @@ const ModeToolsComponent = props => {
</InputGroup>
</div>
);
case Modes.BIT_TEXT:
/* falls through */
case Modes.TEXT:
return (
<div className={classNames(props.className, styles.modeTools)}>

View file

@ -10,7 +10,6 @@ import BitBrushMode from '../../containers/bit-brush-mode.jsx';
import BitLineMode from '../../containers/bit-line-mode.jsx';
import BitOvalMode from '../../containers/bit-oval-mode.jsx';
import BitRectMode from '../../containers/bit-rect-mode.jsx';
import BitTextMode from '../../components/bit-text-mode/bit-text-mode.jsx';
import BitFillMode from '../../containers/bit-fill-mode.jsx';
import BitEraserMode from '../../containers/bit-eraser-mode.jsx';
import BitSelectMode from '../../components/bit-select-mode/bit-select-mode.jsx';
@ -167,7 +166,7 @@ const PaintEditorComponent = props => (
/>
</div>
) : null}
{props.canvas !== null ? ( // eslint-disable-line no-negated-condition
<div className={isBitmap(props.format) ? styles.modeSelector : styles.hidden}>
<BitBrushMode
@ -182,7 +181,11 @@ const PaintEditorComponent = props => (
<BitRectMode
onUpdateImage={props.onUpdateImage}
/>
<BitTextMode />
<TextMode
isBitmap
textArea={props.textArea}
onUpdateImage={props.onUpdateImage}
/>
<BitFillMode
onUpdateImage={props.onUpdateImage}
/>
@ -192,7 +195,7 @@ const PaintEditorComponent = props => (
<BitSelectMode />
</div>
) : null}
<div>
{/* Canvas */}
<div

View file

@ -127,6 +127,9 @@ class PaintEditor extends React.Component {
case Modes.BIT_RECT:
this.props.changeMode(Modes.RECT);
break;
case Modes.BIT_TEXT:
this.props.changeMode(Modes.TEXT);
break;
case Modes.BIT_FILL:
this.props.changeMode(Modes.FILL);
break;
@ -150,6 +153,9 @@ class PaintEditor extends React.Component {
case Modes.RECT:
this.props.changeMode(Modes.BIT_RECT);
break;
case Modes.TEXT:
this.props.changeMode(Modes.BIT_TEXT);
break;
case Modes.FILL:
this.props.changeMode(Modes.BIT_FILL);
break;

View file

@ -17,6 +17,7 @@ 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';
import BitTextModeComponent from '../components/bit-text-mode/bit-text-mode.jsx';
class TextMode extends React.Component {
constructor (props) {
@ -49,7 +50,7 @@ class TextMode extends React.Component {
}
if (nextProps.isTextModeActive && !this.props.isTextModeActive) {
this.activateTool();
this.activateTool(nextProps);
} else if (!nextProps.isTextModeActive && this.props.isTextModeActive) {
this.deactivateTool();
}
@ -57,13 +58,13 @@ class TextMode extends React.Component {
shouldComponentUpdate (nextProps) {
return nextProps.isTextModeActive !== this.props.isTextModeActive;
}
activateTool () {
activateTool (nextProps) {
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 {fillColor, strokeColor, strokeWidth} = nextProps.colorState;
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
const strokeColorPresent =
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
@ -75,8 +76,8 @@ class TextMode extends React.Component {
} else if (fillColorPresent && !strokeColorPresent) {
this.props.onChangeStrokeColor(null);
}
if (!this.props.font || Object.keys(Fonts).map(key => Fonts[key])
.indexOf(this.props.font) < 0) {
if (!nextProps.font || Object.keys(Fonts).map(key => Fonts[key])
.indexOf(nextProps.font) < 0) {
this.props.changeFont(Fonts.SANS_SERIF);
}
@ -86,10 +87,11 @@ class TextMode extends React.Component {
this.props.clearSelectedItems,
this.props.onUpdateImage,
this.props.setTextEditTarget,
this.props.changeFont
this.props.changeFont,
nextProps.isBitmap
);
this.tool.setColorState(this.props.colorState);
this.tool.setFont(this.props.font);
this.tool.setColorState(nextProps.colorState);
this.tool.setFont(nextProps.font);
this.tool.activate();
}
deactivateTool () {
@ -99,10 +101,15 @@ class TextMode extends React.Component {
}
render () {
return (
<TextModeComponent
isSelected={this.props.isTextModeActive}
onMouseDown={this.props.handleMouseDown}
/>
this.props.isBitmap ?
<BitTextModeComponent
isSelected={this.props.isTextModeActive}
onMouseDown={this.props.handleChangeModeBitText}
/> :
<TextModeComponent
isSelected={this.props.isTextModeActive}
onMouseDown={this.props.handleChangeModeText}
/>
);
}
}
@ -116,7 +123,9 @@ TextMode.propTypes = {
strokeWidth: PropTypes.number
}).isRequired,
font: PropTypes.string,
handleMouseDown: PropTypes.func.isRequired,
handleChangeModeBitText: PropTypes.func.isRequired,
handleChangeModeText: PropTypes.func.isRequired,
isBitmap: PropTypes.bool,
isTextModeActive: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired,
onChangeStrokeColor: PropTypes.func.isRequired,
@ -129,10 +138,12 @@ TextMode.propTypes = {
viewBounds: PropTypes.instanceOf(paper.Matrix).isRequired
};
const mapStateToProps = state => ({
const mapStateToProps = (state, ownProps) => ({
colorState: state.scratchPaint.color,
font: state.scratchPaint.font,
isTextModeActive: state.scratchPaint.mode === Modes.TEXT,
isTextModeActive: ownProps.isBitmap ?
state.scratchPaint.mode === Modes.BIT_TEXT :
state.scratchPaint.mode === Modes.TEXT,
selectedItems: state.scratchPaint.selectedItems,
textEditTarget: state.scratchPaint.textEditTarget,
viewBounds: state.scratchPaint.viewBounds
@ -144,15 +155,18 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
handleChangeModeBitText: () => {
dispatch(changeMode(Modes.BIT_TEXT));
},
handleChangeModeText: () => {
dispatch(changeMode(Modes.TEXT));
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems()));
},
setTextEditTarget: targetId => {
dispatch(setTextEditTarget(targetId));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.TEXT));
},
onChangeFillColor: fillColor => {
dispatch(changeFillColor(fillColor));
},

View file

@ -9,13 +9,13 @@ const forEachLinePoint = function (point1, point2, callback) {
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;
@ -336,7 +336,7 @@ const convertToBitmap = function (clearSelectedItems, onUpdateImage) {
showGuideLayers(guideLayers);
// Get rid of anti-aliasing
// @todo get crisp text?
// @todo get crisp text https://github.com/LLK/scratch-paint/issues/508
svg.setAttribute('shape-rendering', 'crispEdges');
inlineSvgFonts(svg);
const svgString = (new XMLSerializer()).serializeToString(svg);

View file

@ -4,6 +4,7 @@ import {clearSelection, getSelectedLeafItems} from '../selection';
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
import NudgeTool from '../selection-tools/nudge-tool';
import {hoverBounds} from '../guides';
import {getRaster} from '../layer';
/**
* Tool for adding text. Text elements have limited editability; they can't be reshaped,
@ -37,8 +38,10 @@ class TextTool extends paper.Tool {
* @param {!function} onUpdateImage A callback to call when the image visibly changes
* @param {!function} setTextEditTarget Call to set text editing target whenever text editing is active
* @param {!function} changeFont Call to change the font in the dropdown
* @param {?boolean} isBitmap True if text should be rasterized once it's deselected
*/
constructor (textAreaElement, setSelectedItems, clearSelectedItems, onUpdateImage, setTextEditTarget, changeFont) {
constructor (textAreaElement, setSelectedItems, clearSelectedItems, onUpdateImage, setTextEditTarget, changeFont,
isBitmap) {
super();
this.element = textAreaElement;
this.setSelectedItems = setSelectedItems;
@ -48,8 +51,8 @@ class TextTool extends paper.Tool {
this.changeFont = changeFont;
this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateImage);
this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage);
this.lastEvent = null;
this.isBitmap = isBitmap;
// 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;
@ -65,6 +68,7 @@ class TextTool extends paper.Tool {
this.mode = null;
this.active = false;
this.lastTypeEvent = null;
this.lastEvent = null;
// If text selected and then activate this tool, switch to text edit mode for that text
// If double click on text while in select mode, does mode change to text mode? Text fully selected by default
@ -100,6 +104,12 @@ class TextTool extends paper.Tool {
*/
onSelectionChanged (selectedItems) {
this.boundingBoxTool.onSelectionChanged(selectedItems);
if ((!this.textBox || !this.textBox.parent) &&
selectedItems && selectedItems.length === 1 && selectedItems[0] instanceof paper.PointText) {
// Infer that an undo occurred and get back the active text
this.textBox = selectedItems[0];
this.mode = TextTool.SELECT_MODE;
}
}
setFont (font) {
this.font = font;
@ -117,12 +127,11 @@ class TextTool extends paper.Tool {
}
// Allow other tools to cancel text edit mode
onTextEditCancelled () {
this.endTextEdit();
if (this.textBox) {
this.mode = TextTool.SELECT_MODE;
this.textBox.selected = true;
this.setSelectedItems();
if (this.mode !== TextTool.TEXT_EDIT_MODE) {
return;
}
this.endTextEdit();
this.beginSelect();
}
/**
* Called when the view matrix changes
@ -155,55 +164,44 @@ class TextTool extends paper.Tool {
if (event.event.button > 0) return; // only first mouse button
this.active = true;
const lastMode = this.mode;
// Check if double clicked
let doubleClicked = false;
if (this.lastEvent) {
if ((event.event.timeStamp - this.lastEvent.event.timeStamp) < TextTool.DOUBLE_CLICK_MILLIS) {
doubleClicked = true;
} else {
doubleClicked = false;
}
}
const doubleClicked = this.lastEvent &&
(event.event.timeStamp - this.lastEvent.event.timeStamp) < TextTool.DOUBLE_CLICK_MILLIS;
this.lastEvent = event;
const doubleClickHitTest = paper.project.hitTest(event.point, this.getBoundingBoxHitOptions());
if (doubleClicked &&
this.mode === TextTool.SELECT_MODE &&
doubleClickHitTest) {
this.textBox.hitTest(event.point)) {
// Double click in select mode moves you to text edit mode
clearSelection(this.clearSelectedItems);
this.textBox = doubleClickHitTest.item;
this.endSelect();
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
} else if (
this.boundingBoxTool.onMouseDown(
event, false /* clone */, false /* multiselect */, this.getBoundingBoxHitOptions())) {
// In select mode staying in select mode
return;
}
// In select mode staying in select mode
if (this.boundingBoxTool.onMouseDown(
event, false /* clone */, false /* multiselect */, this.getBoundingBoxHitOptions())) {
return;
}
// We clicked away from the item, so end the current mode
if (lastMode === TextTool.SELECT_MODE) {
clearSelection(this.clearSelectedItems);
this.mode = null;
} else if (lastMode === TextTool.TEXT_EDIT_MODE) {
const lastMode = this.mode;
if (this.mode === TextTool.SELECT_MODE) {
this.endSelect();
if (this.isBitmap) {
this.commitText();
}
} else if (this.mode === TextTool.TEXT_EDIT_MODE) {
this.endTextEdit();
}
const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions());
if (hitResults.length) {
// Clicking a different text item to begin text edit mode on that item
clearSelection(this.clearSelectedItems);
this.textBox = hitResults[0].item;
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
} else if (lastMode === TextTool.TEXT_EDIT_MODE) {
// In text mode clicking away to begin select mode
if (this.textBox) {
this.mode = TextTool.SELECT_MODE;
this.textBox.selected = true;
this.setSelectedItems();
}
this.beginSelect();
} else {
// In no mode or select mode clicking away to begin text edit mode
this.textBox = new paper.PointText({
@ -230,7 +228,7 @@ class TextTool extends paper.Tool {
}
handleMouseUp (event) {
if (event.event.button > 0 || !this.active) return; // only first mouse button
if (this.mode === TextTool.SELECT_MODE) {
this.boundingBoxTool.onMouseUp(event);
this.isBoundingBoxMode = null;
@ -264,7 +262,10 @@ class TextTool extends paper.Tool {
handleTextInput (event) {
// Save undo state if you paused typing for long enough.
if (this.lastTypeEvent && event.timeStamp - this.lastTypeEvent.timeStamp > TextTool.TYPING_TIMEOUT_MILLIS) {
// Select the textbox so that it will be selected if the user performs undo.
this.textBox.selected = true;
this.onUpdateImage();
this.textBox.selected = false;
}
this.lastTypeEvent = event;
if (this.mode === TextTool.TEXT_EDIT_MODE) {
@ -280,6 +281,17 @@ class TextTool extends paper.Tool {
this.element.style.width = `${this.textBox.internalBounds.width + 1}px`;
this.element.style.height = `${this.textBox.internalBounds.height}px`;
}
beginSelect () {
if (this.textBox) {
this.mode = TextTool.SELECT_MODE;
this.textBox.selected = true;
this.setSelectedItems();
}
}
endSelect () {
clearSelection(this.clearSelectedItems);
this.mode = null;
}
/**
* @param {string} initialText Text to initialize the text area with
* @param {paper.Matrix} matrix Transform matrix for the element. Defaults
@ -334,19 +346,37 @@ class TextTool extends paper.Tool {
this.element.removeEventListener('input', this.eventListener);
this.eventListener = null;
}
this.lastTypeEvent = null;
// If you finished editing a textbox, save undo state
if (this.textBox && this.textBox.content.trim().length) {
if (this.textBox && this.lastTypeEvent) {
// Finished editing a textbox, save undo state
// Select the textbox so that it will be selected if the user performs undo.
this.textBox.selected = true;
this.onUpdateImage();
this.textBox.selected = false;
this.lastTypeEvent = null;
}
}
commitText () {
if (!this.textBox || !this.textBox.parent) return;
// @todo get crisp text https://github.com/LLK/scratch-paint/issues/508
const textRaster = this.textBox.rasterize(72, false /* insert */);
this.textBox.remove();
this.textBox = null;
getRaster().drawImage(
textRaster.canvas,
new paper.Point(Math.floor(textRaster.bounds.x), Math.floor(textRaster.bounds.y))
);
this.onUpdateImage();
}
deactivateTool () {
if (this.textBox && this.textBox.content.trim() === '') {
this.textBox.remove();
this.textBox = null;
}
this.endTextEdit();
if (this.isBitmap) {
this.commitText();
}
this.boundingBoxTool.removeBoundsPath();
}
}

View file

@ -5,6 +5,7 @@ const Modes = keyMirror({
BIT_LINE: null,
BIT_OVAL: null,
BIT_RECT: null,
BIT_TEXT: null,
BIT_FILL: null,
BIT_ERASER: null,
BRUSH: null,
@ -24,6 +25,7 @@ const BitmapModes = keyMirror({
BIT_LINE: null,
BIT_OVAL: null,
BIT_RECT: null,
BIT_TEXT: null,
BIT_FILL: null,
BIT_ERASER: null
});