From 0240abcfe3a0c0292defa828c20814a7dd2f043d Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 17 May 2018 10:37:02 -0400 Subject: [PATCH] Font tool (#443) Add font picker --- src/components/dropdown/dropdown.jsx | 17 +- .../font-dropdown/font-dropdown.css | 80 ++++++ .../font-dropdown/font-dropdown.jsx | 129 ++++++++++ src/components/mode-tools/mode-tools.jsx | 12 + src/components/paint-editor/paint-editor.css | 2 - src/containers/font-dropdown.jsx | 231 ++++++++++++++++++ src/containers/mode-tools.jsx | 1 + src/containers/text-mode.jsx | 19 ++ src/helper/tools/text-tool.js | 27 +- src/lib/fonts.js | 13 + src/reducers/font.js | 28 +++ src/reducers/scratch-paint-reducer.js | 2 + 12 files changed, 553 insertions(+), 8 deletions(-) create mode 100644 src/components/font-dropdown/font-dropdown.css create mode 100644 src/components/font-dropdown/font-dropdown.jsx create mode 100644 src/containers/font-dropdown.jsx create mode 100644 src/lib/fonts.js create mode 100644 src/reducers/font.js diff --git a/src/components/dropdown/dropdown.jsx b/src/components/dropdown/dropdown.jsx index 497d4e89..a8ec3f95 100644 --- a/src/components/dropdown/dropdown.jsx +++ b/src/components/dropdown/dropdown.jsx @@ -13,7 +13,8 @@ class Dropdown extends React.Component { super(props); bindAll(this, [ 'handleClosePopover', - 'handleToggleOpenState' + 'handleToggleOpenState', + 'isOpen' ]); this.state = { isOpen: false @@ -25,9 +26,16 @@ class Dropdown extends React.Component { }); } handleToggleOpenState () { + const newState = !this.state.isOpen; this.setState({ - isOpen: !this.state.isOpen + isOpen: newState }); + if (newState && this.props.onOpen) { + this.props.onOpen(); + } + } + isOpen () { + return this.state.isOpen; } render () { return ( @@ -35,7 +43,8 @@ class Dropdown extends React.Component { body={this.props.popoverContent} isOpen={this.state.isOpen} preferPlace="below" - onOuterAction={this.handleClosePopover} + onOuterAction={this.props.onOuterAction ? + this.props.onOuterAction : this.handleClosePopover} {...this.props} >
( + + + + + + + + + + + + } + ref={props.componentRef} + tipSize={.01} + onOpen={props.onOpenDropdown} + onOuterAction={props.onClickOutsideDropdown} + > + + {props.getTranslatedFontName(props.font)} + + +); + +ModeToolsComponent.propTypes = { + componentRef: PropTypes.func.isRequired, + font: PropTypes.string, + getFontStyle: PropTypes.func.isRequired, + getTranslatedFontName: PropTypes.func.isRequired, + onChoose: PropTypes.func.isRequired, + onClickOutsideDropdown: PropTypes.func, + onHoverChinese: PropTypes.func, + onHoverCurly: PropTypes.func, + onHoverHandwriting: PropTypes.func, + onHoverJapanese: PropTypes.func, + onHoverKorean: PropTypes.func, + onHoverMarker: PropTypes.func, + onHoverPixel: PropTypes.func, + onHoverSansSerif: PropTypes.func, + onHoverSerif: PropTypes.func, + onOpenDropdown: PropTypes.func +}; +export default ModeToolsComponent; diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index 80523e06..8ccaa600 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -8,6 +8,7 @@ import {changeBrushSize} from '../../reducers/brush-mode'; import {changeBrushSize as changeEraserSize} from '../../reducers/eraser-mode'; import {changeBitBrushSize} from '../../reducers/bit-brush-size'; +import FontDropdown from '../../containers/font-dropdown.jsx'; import LiveInputHOC from '../forms/live-input-hoc.jsx'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import Input from '../forms/input.jsx'; @@ -186,6 +187,16 @@ const ModeToolsComponent = props => {
); + case Modes.TEXT: + return ( +
+ + + +
+ ); default: // Leave empty for now, if mode not supported return ( @@ -214,6 +225,7 @@ ModeToolsComponent.propTypes = { onFlipVertical: PropTypes.func.isRequired, onPasteFromClipboard: PropTypes.func.isRequired, onPointPoints: PropTypes.func.isRequired, + onUpdateImage: PropTypes.func.isRequired, selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)) }; diff --git a/src/components/paint-editor/paint-editor.css b/src/components/paint-editor/paint-editor.css index 1b6415ff..bdb4646f 100644 --- a/src/components/paint-editor/paint-editor.css +++ b/src/components/paint-editor/paint-editor.css @@ -168,8 +168,6 @@ $border-radius: 0.25rem; background: transparent; border: none; display: none; - font-family: Helvetica; - font-size: 30px; outline: none; overflow: hidden; padding: 0px; diff --git a/src/containers/font-dropdown.jsx b/src/containers/font-dropdown.jsx new file mode 100644 index 00000000..7c7f0214 --- /dev/null +++ b/src/containers/font-dropdown.jsx @@ -0,0 +1,231 @@ +import paper from '@scratch/paper'; +import {connect} from 'react-redux'; +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {defineMessages, injectIntl, intlShape} from 'react-intl'; + +import FontDropdownComponent from '../components/font-dropdown/font-dropdown.jsx'; +import Fonts from '../lib/fonts'; +import {changeFont} from '../reducers/font'; +import {getSelectedLeafItems} from '../helper/selection'; +import styles from '../components/font-dropdown/font-dropdown.css'; + +const messages = defineMessages({ + sansSerif: { + defaultMessage: 'Sans Serif', + description: 'Name of the sans serif font', + id: 'paint.modeTools.sansSerif' + }, + serif: { + defaultMessage: 'Serif', + description: 'Name of the serif font', + id: 'paint.modeTools.serif' + }, + handwriting: { + defaultMessage: 'Handwriting', + description: 'Name of the handwriting font', + id: 'paint.modeTools.handwriting' + }, + marker: { + defaultMessage: 'Marker', + description: 'Name of the marker font', + id: 'paint.modeTools.marker' + }, + curly: { + defaultMessage: 'Curly', + description: 'Name of the curly font', + id: 'paint.modeTools.curly' + }, + pixel: { + defaultMessage: 'Pixel', + description: 'Name of the pixelated font', + id: 'paint.modeTools.pixel' + } +}); +class ModeToolsComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'getFontStyle', + 'getTranslatedFontName', + 'handleChangeFontSerif', + 'handleChangeFontSansSerif', + 'handleChangeFontHandwriting', + 'handleChangeFontMarker', + 'handleChangeFontCurly', + 'handleChangeFontPixel', + 'handleChangeFontChinese', + 'handleChangeFontJapanese', + 'handleChangeFontKorean', + 'handleOpenDropdown', + 'handleClickOutsideDropdown', + 'setDropdown', + 'handleChoose' + ]); + } + getFontStyle (font) { + switch (font) { + case Fonts.SERIF: + return styles.serif; + case Fonts.SANS_SERIF: + return styles.sansSerif; + case Fonts.HANDWRITING: + return styles.handwriting; + case Fonts.MARKER: + return styles.marker; + case Fonts.CURLY: + return styles.curly; + case Fonts.PIXEL: + return styles.pixel; + case Fonts.CHINESE: + return styles.chinese; + case Fonts.JAPANESE: + return styles.japanese; + case Fonts.KOREAN: + return styles.korean; + default: + return ''; + } + } + getTranslatedFontName (font) { + switch (font) { + case Fonts.SERIF: + return this.props.intl.formatMessage(messages.serif); + case Fonts.SANS_SERIF: + return this.props.intl.formatMessage(messages.sansSerif); + case Fonts.HANDWRITING: + return this.props.intl.formatMessage(messages.handwriting); + case Fonts.MARKER: + return this.props.intl.formatMessage(messages.marker); + case Fonts.CURLY: + return this.props.intl.formatMessage(messages.curly); + case Fonts.PIXEL: + return this.props.intl.formatMessage(messages.pixel); + case Fonts.CHINESE: + return '中文'; + case Fonts.KOREAN: + return '한국어'; + case Fonts.JAPANESE: + return '日本語'; + default: + return font; + } + } + handleChangeFontSansSerif () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.SANS_SERIF); + } + } + handleChangeFontSerif () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.SERIF); + } + } + handleChangeFontHandwriting () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.HANDWRITING); + } + } + handleChangeFontMarker () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.MARKER); + } + } + handleChangeFontCurly () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.CURLY); + } + } + handleChangeFontPixel () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.PIXEL); + } + } + handleChangeFontChinese () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.CHINESE); + } + } + handleChangeFontJapanese () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.JAPANESE); + } + } + handleChangeFontKorean () { + if (this.dropDown.isOpen()) { + this.props.changeFont(Fonts.KOREAN); + } + } + handleChoose () { + if (this.dropDown.isOpen()) { + this.dropDown.handleClosePopover(); + this.props.onUpdateImage(); + } + } + handleOpenDropdown () { + this.savedFont = this.props.font; + this.savedSelection = getSelectedLeafItems(); + } + handleClickOutsideDropdown (e) { + e.stopPropagation(); + this.dropDown.handleClosePopover(); + + // Cancel font change + for (const item of this.savedSelection) { + if (item instanceof paper.PointText) { + item.font = this.savedFont; + } + } + + this.props.changeFont(this.savedFont); + this.savedFont = null; + this.savedSelection = null; + } + setDropdown (element) { + this.dropDown = element; + } + render () { + return ( + + ); + } +} + +ModeToolsComponent.propTypes = { + changeFont: PropTypes.func.isRequired, + font: PropTypes.string, + intl: intlShape.isRequired, + onUpdateImage: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + font: state.scratchPaint.font +}); +const mapDispatchToProps = dispatch => ({ + changeFont: font => { + dispatch(changeFont(font)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(injectIntl(ModeToolsComponent)); diff --git a/src/containers/mode-tools.jsx b/src/containers/mode-tools.jsx index fcf3ee24..3b9df8fe 100644 --- a/src/containers/mode-tools.jsx +++ b/src/containers/mode-tools.jsx @@ -208,6 +208,7 @@ class ModeTools extends React.Component { onFlipVertical={this.handleFlipVertical} onPasteFromClipboard={this.handlePasteFromClipboard} onPointPoints={this.handlePointPoints} + onUpdateImage={this.props.onUpdateImage} /> ); } diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx index 39dd7f35..54a8c455 100644 --- a/src/containers/text-mode.jsx +++ b/src/containers/text-mode.jsx @@ -3,9 +3,11 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; +import Fonts from '../lib/fonts'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; +import {changeFont} from '../reducers/font'; import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; import {changeStrokeColor} from '../reducers/stroke-color'; import {changeMode} from '../reducers/modes'; @@ -42,6 +44,9 @@ class TextMode extends React.Component { if (this.tool && !nextProps.viewBounds.equals(this.props.viewBounds)) { this.tool.onViewBoundsChanged(nextProps.viewBounds); } + if (this.tool && nextProps.font !== this.props.font) { + this.tool.setFont(nextProps.font); + } if (nextProps.isTextModeActive && !this.props.isTextModeActive) { this.activateTool(); @@ -54,6 +59,7 @@ class TextMode extends React.Component { } 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. @@ -69,14 +75,21 @@ 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) { + this.props.changeFont(Fonts.SANS_SERIF); + } + this.tool = new TextTool( this.props.textArea, this.props.setSelectedItems, this.props.clearSelectedItems, this.props.onUpdateImage, this.props.setTextEditTarget, + this.props.changeFont ); this.tool.setColorState(this.props.colorState); + this.tool.setFont(this.props.font); this.tool.activate(); } deactivateTool () { @@ -95,12 +108,14 @@ class TextMode extends React.Component { } TextMode.propTypes = { + changeFont: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ fillColor: PropTypes.string, strokeColor: PropTypes.string, strokeWidth: PropTypes.number }).isRequired, + font: PropTypes.string, handleMouseDown: PropTypes.func.isRequired, isTextModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, @@ -116,12 +131,16 @@ TextMode.propTypes = { const mapStateToProps = state => ({ colorState: state.scratchPaint.color, + font: state.scratchPaint.font, isTextModeActive: state.scratchPaint.mode === Modes.TEXT, selectedItems: state.scratchPaint.selectedItems, textEditTarget: state.scratchPaint.textEditTarget, viewBounds: state.scratchPaint.viewBounds }); const mapDispatchToProps = dispatch => ({ + changeFont: font => { + dispatch(changeFont(font)); + }, clearSelectedItems: () => { dispatch(clearSelectedItems()); }, diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 006faab1..e9548d76 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; -import {clearSelection} from '../selection'; +import {clearSelection, getSelectedLeafItems} from '../selection'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; import {hoverBounds} from '../guides'; @@ -36,14 +36,16 @@ class TextTool extends paper.Tool { * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state * @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 */ - constructor (textAreaElement, setSelectedItems, clearSelectedItems, onUpdateImage, setTextEditTarget) { + constructor (textAreaElement, setSelectedItems, clearSelectedItems, onUpdateImage, setTextEditTarget, changeFont) { super(); this.element = textAreaElement; this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.onUpdateImage = onUpdateImage; this.setTextEditTarget = setTextEditTarget; + this.changeFont = changeFont; this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateImage); this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); this.lastEvent = null; @@ -99,6 +101,20 @@ class TextTool extends paper.Tool { onSelectionChanged (selectedItems) { this.boundingBoxTool.onSelectionChanged(selectedItems); } + setFont (font) { + this.font = font; + if (this.textBox) { + this.textBox.font = font; + } + const selected = getSelectedLeafItems(); + for (const item of selected) { + if (item instanceof paper.PointText) { + item.font = font; + } + } + this.element.style.fontFamily = font; + this.setSelectedItems(); + } // Allow other tools to cancel text edit mode onTextEditCancelled () { this.endTextEdit(); @@ -193,7 +209,7 @@ class TextTool extends paper.Tool { this.textBox = new paper.PointText({ point: event.point, content: '', - font: 'Helvetica', + font: this.font, fontSize: 30, fillColor: this.colorState.fillColor, // Default leading for both the HTML text area and paper.PointText @@ -272,6 +288,11 @@ class TextTool extends paper.Tool { beginTextEdit (initialText, matrix) { this.mode = TextTool.TEXT_EDIT_MODE; this.setTextEditTarget(this.textBox.id); + if (this.font !== this.textBox.font) { + this.changeFont(this.textBox.font); + } + this.element.style.fontSize = `${this.textBox.fontSize}px`; + this.element.style.lineHeight = this.textBox.leading / this.textBox.fontSize; const viewMtx = paper.view.matrix; diff --git a/src/lib/fonts.js b/src/lib/fonts.js new file mode 100644 index 00000000..93fec3c6 --- /dev/null +++ b/src/lib/fonts.js @@ -0,0 +1,13 @@ +const Fonts = { + SANS_SERIF: 'Sans Serif', + SERIF: 'Serif', + HANDWRITING: 'Handwriting', + MARKER: 'Marker', + CURLY: 'Curly', + PIXEL: 'Pixel', + CHINESE: '"Microsoft YaHei", "微软雅黑", STXihei, "华文细黑"', + JAPANESE: '"ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Osaka, "メイリオ", Meiryo, "MS Pゴシック", "MS PGothic"', + KOREAN: 'Malgun Gothic' +}; + +export default Fonts; diff --git a/src/reducers/font.js b/src/reducers/font.js new file mode 100644 index 00000000..1727bc3b --- /dev/null +++ b/src/reducers/font.js @@ -0,0 +1,28 @@ +import Fonts from '../lib/fonts'; + +const CHANGE_FONT = 'scratch-paint/fonts/CHANGE_FONT'; +const initialState = Fonts.SANS_SERIF; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_FONT: + if (!action.font) return state; + return action.font; + default: + return state; + } +}; + +// Action creators ================================== +const changeFont = function (font) { + return { + type: CHANGE_FONT, + font: font + }; +}; + +export { + reducer as default, + changeFont +}; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 3527051d..31dcb476 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -5,6 +5,7 @@ import brushModeReducer from './brush-mode'; import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; import clipboardReducer from './clipboard'; +import fontReducer from './font'; import formatReducer from './format'; import hoverReducer from './hover'; import modalsReducer from './modals'; @@ -20,6 +21,7 @@ export default combineReducers({ color: colorReducer, clipboard: clipboardReducer, eraserMode: eraserModeReducer, + font: fontReducer, format: formatReducer, hoveredItemId: hoverReducer, modals: modalsReducer,