From d7298c0c4327db106d2ded583a48b502ac17b14a Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 19 Jun 2018 14:54:29 -0400 Subject: [PATCH] Bit text tool (#515) --- .../bit-text-mode/bit-text-mode.jsx | 35 +++--- src/components/mode-tools/mode-tools.jsx | 2 + src/components/paint-editor/paint-editor.jsx | 11 +- src/containers/paint-editor.jsx | 6 + src/containers/text-mode.jsx | 52 +++++--- src/helper/bitmap.js | 6 +- src/helper/tools/text-tool.js | 114 +++++++++++------- src/lib/modes.js | 2 + 8 files changed, 142 insertions(+), 86 deletions(-) diff --git a/src/components/bit-text-mode/bit-text-mode.jsx b/src/components/bit-text-mode/bit-text-mode.jsx index 4e129ca7..2a0cc405 100644 --- a/src/components/bit-text-mode/bit-text-mode.jsx +++ b/src/components/bit-text-mode/bit-text-mode.jsx @@ -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 = () => ( - - - +const BitTextComponent = props => ( + ); +BitTextComponent.propTypes = { + isSelected: PropTypes.bool.isRequired, + onMouseDown: PropTypes.func.isRequired +}; + export default BitTextComponent; diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index e0edbf53..dd4b8c7c 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -196,6 +196,8 @@ const ModeToolsComponent = props => { ); + case Modes.BIT_TEXT: + /* falls through */ case Modes.TEXT: return (
diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 7aa5bd18..8435bbaf 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -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 => ( />
) : null} - + {props.canvas !== null ? ( // eslint-disable-line no-negated-condition
( - + @@ -192,7 +195,7 @@ const PaintEditorComponent = props => (
) : null} - +
{/* Canvas */}
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 ( - + this.props.isBitmap ? + : + ); } } @@ -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)); }, diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 6fefd6a5..87e38626 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -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); diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 8e009b40..35921829 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -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(); } } diff --git a/src/lib/modes.js b/src/lib/modes.js index 29927376..1f41016e 100644 --- a/src/lib/modes.js +++ b/src/lib/modes.js @@ -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 });