Font tool (#443)

Add font picker
This commit is contained in:
DD Liu 2018-05-17 10:37:02 -04:00 committed by GitHub
parent 7f216defd2
commit 0240abcfe3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 553 additions and 8 deletions

View file

@ -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}
>
<div
@ -62,6 +71,8 @@ class Dropdown extends React.Component {
Dropdown.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
onOpen: PropTypes.func,
onOuterAction: PropTypes.func,
popoverContent: PropTypes.node.isRequired
};

View file

@ -0,0 +1,80 @@
@import "../../css/colors.css";
@import "../../css/units.css";
.mod-menu-item {
display: flex;
margin: 0 -$grid-unit;
min-width: 6.25rem;
padding: calc(2 * $grid-unit);
padding-left: calc(3 * $grid-unit);
padding-right: calc(3 * $grid-unit);
white-space: nowrap;
width: 8.5rem;
cursor: pointer;
transition: 0.1s ease;
align-items: center;
}
.mod-menu-item:hover {
background: $motion-primary;
color: white;
}
.mod-context-menu {
display: flex;
flex-direction: column;
}
.mod-unselect {
user-select: none;
}
.font-dropdown {
align-items: center;
color: $text-primary;
display: flex;
font-size: 0.625rem;
justify-content: space-between;
width: 8.5rem;
height: 2rem;
}
.serif {
font-family: 'Serif';
}
.sans-serif {
font-family: 'Sans Serif';
}
.serif {
font-family: 'Serif';
}
.handwriting {
font-family: 'Handwriting';
}
.marker {
font-family: 'Marker';
}
.curly {
font-family: 'Curly';
}
.pixel {
font-family: 'Pixel';
}
.chinese {
font-family: "Microsoft YaHei", "微软雅黑", STXihei, "华文细黑";
}
.japanese {
font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Osaka, "メイリオ", Meiryo, " Pゴシック", "MS PGothic";
}
.korean {
font-family: "Malgun Gothic";
}

View file

@ -0,0 +1,129 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Button from '../button/button.jsx';
import Dropdown from '../dropdown/dropdown.jsx';
import InputGroup from '../input-group/input-group.jsx';
import Fonts from '../../lib/fonts';
import styles from './font-dropdown.css';
const ModeToolsComponent = props => (
<Dropdown
className={classNames(styles.modUnselect, styles.fontDropdown)}
enterExitTransitionDurationMs={60}
popoverContent={
<InputGroup className={styles.modContextMenu}>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverSansSerif}
>
<span className={styles.sansSerif}>
{props.getTranslatedFontName(Fonts.SANS_SERIF)}
</span>
</Button>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverSerif}
>
<span className={styles.serif}>
{props.getTranslatedFontName(Fonts.SERIF)}
</span>
</Button>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverHandwriting}
>
<span className={styles.handwriting}>
{props.getTranslatedFontName(Fonts.HANDWRITING)}
</span>
</Button>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverMarker}
>
<span className={styles.marker}>
{props.getTranslatedFontName(Fonts.MARKER)}
</span>
</Button>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverCurly}
>
<span className={styles.curly}>
{props.getTranslatedFontName(Fonts.CURLY)}
</span>
</Button>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverPixel}
>
<span className={styles.pixel}>
{props.getTranslatedFontName(Fonts.PIXEL)}
</span>
</Button>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverChinese}
>
<span className={styles.chinese}>
{props.getTranslatedFontName(Fonts.CHINESE)}
</span>
</Button>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverJapanese}
>
<span className={styles.japanese}>
{props.getTranslatedFontName(Fonts.JAPANESE)}
</span>
</Button>
<Button
className={classNames(styles.modMenuItem)}
onClick={props.onChoose}
onMouseOver={props.onHoverKorean}
>
<span className={styles.korean}>
{props.getTranslatedFontName(Fonts.KOREAN)}
</span>
</Button>
</InputGroup>
}
ref={props.componentRef}
tipSize={.01}
onOpen={props.onOpenDropdown}
onOuterAction={props.onClickOutsideDropdown}
>
<span className={props.getFontStyle(props.font)}>
{props.getTranslatedFontName(props.font)}
</span>
</Dropdown>
);
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;

View file

@ -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 => {
</InputGroup>
</div>
);
case Modes.TEXT:
return (
<div className={classNames(props.className, styles.modeTools)}>
<InputGroup>
<FontDropdown
onUpdateImage={props.onUpdateImage}
/>
</InputGroup>
</div>
);
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))
};

View file

@ -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;

View file

@ -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 (
<FontDropdownComponent
componentRef={this.setDropdown}
font={this.props.font}
getFontStyle={this.getFontStyle}
getTranslatedFontName={this.getTranslatedFontName}
onChoose={this.handleChoose}
onClickOutsideDropdown={this.handleClickOutsideDropdown}
onHoverChinese={this.handleChangeFontChinese}
onHoverCurly={this.handleChangeFontCurly}
onHoverHandwriting={this.handleChangeFontHandwriting}
onHoverJapanese={this.handleChangeFontJapanese}
onHoverKorean={this.handleChangeFontKorean}
onHoverMarker={this.handleChangeFontMarker}
onHoverPixel={this.handleChangeFontPixel}
onHoverSansSerif={this.handleChangeFontSansSerif}
onHoverSerif={this.handleChangeFontSerif}
onOpenDropdown={this.handleOpenDropdown}
/>
);
}
}
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));

View file

@ -208,6 +208,7 @@ class ModeTools extends React.Component {
onFlipVertical={this.handleFlipVertical}
onPasteFromClipboard={this.handlePasteFromClipboard}
onPointPoints={this.handlePointPoints}
onUpdateImage={this.props.onUpdateImage}
/>
);
}

View file

@ -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());
},

View file

@ -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;

13
src/lib/fonts.js Normal file
View file

@ -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, " Pゴシック", "MS PGothic"',
KOREAN: 'Malgun Gothic'
};
export default Fonts;

28
src/reducers/font.js Normal file
View file

@ -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
};

View file

@ -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,