mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-23 05:52:42 -05:00
commit
3a86ffe642
22 changed files with 813 additions and 123 deletions
13
.github/CONTRIBUTING.md
vendored
Normal file
13
.github/CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
## Contributing
|
||||
The development of Scratch is an ongoing process, and we love to have people in the Scratch and open source communities help us along the way.
|
||||
|
||||
If you're interested in contributing, please take a look at the [issues](https://github.com/LLK/scratch-vm/issues) on this repository.
|
||||
Two great ways of helping are by identifying bugs and documenting them as issues, or fixing issues and creating pull requests. When looking for bugs to fix, please look for the ["Help Wanted" label](https://github.com/LLK/scratch-vm/issues?q=label%3A%22help+wanted%22). Bugs with this label have been specifically set aside for Open Source contributors. Issues without the label can also be worked on but we ask that you comment on the issue prior to starting work. When submitting pull requests please be patient -- it can take a while to find time to review them. The organization and class structures can't be radically changed without significant coordination and collaboration from the Scratch Team, so these types of changes should be avoided.
|
||||
|
||||
It's been said that the Scratch Team spends about one hour of design discussion for every pixel in Scratch, but some think that estimate is a little low. While we welcome suggestions for new features in our [suggestions forum](https://scratch.mit.edu/discuss/1/) (especially ones that come with mockups), we are unlikely to accept PRs with new features that haven't been thought through and discussed as a group. Why? Because we have a strong belief in the value of keeping things simple for new users. To learn more about our design philosophy, see [the Scratch Developers page](https://scratch.mit.edu/developers), or [this paper](http://web.media.mit.edu/~mres/papers/Scratch-CACM-final.pdf).
|
||||
|
||||
Beyond this repo, there are also some other resources that you might want to take a look at:
|
||||
* [Community Guidelines](https://github.com/LLK/scratch-www/wiki/Community-Guidelines) (we find it important to maintain a constructive and welcoming community, just like on Scratch)
|
||||
* [Open Source forum](https://scratch.mit.edu/discuss/49/) on Scratch
|
||||
* [Suggestions forum](https://scratch.mit.edu/discuss/1/) on Scratch
|
||||
* [Bugs & Glitches forum](https://scratch.mit.edu/discuss/3/) on Scratch
|
15
.github/ISSUE_TEMPLATE.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
### Expected Behavior
|
||||
|
||||
_Please describe what should happen_
|
||||
|
||||
### Actual Behavior
|
||||
|
||||
_Describe what actually happens_
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
_Explain what someone needs to do in order to see what's described in *Actual behavior* above_
|
||||
|
||||
### Operating System and Browser
|
||||
|
||||
_e.g. Mac OS 10.11.6 Safari 10.0_
|
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
### Resolves
|
||||
|
||||
_What Github issue does this resolve (please include link)?_
|
||||
|
||||
### Proposed Changes
|
||||
|
||||
_Describe what this Pull Request does_
|
||||
|
||||
### Reason for Changes
|
||||
|
||||
_Explain why these changes should be made_
|
||||
|
||||
### Test Coverage
|
||||
|
||||
_Please show how you have added tests to cover your changes_
|
|
@ -26,7 +26,7 @@
|
|||
"react-dom": "^16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scratch/paper": "~0.11.6",
|
||||
"@scratch/paper": "0.11.20180329192534",
|
||||
"autoprefixer": "8.1.0",
|
||||
"babel-cli": "6.26.0",
|
||||
"babel-core": "^6.23.1",
|
||||
|
|
|
@ -203,3 +203,20 @@ $border-radius: 0.25rem;
|
|||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.text-area {
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: none;
|
||||
font-family: Helvetica;
|
||||
font-size: 30px;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
padding: 0px;
|
||||
position: absolute;
|
||||
resize: none;
|
||||
white-space: nowrap;
|
||||
margin: 0px;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import ReshapeMode from '../../containers/reshape-mode.jsx';
|
|||
import SelectMode from '../../containers/select-mode.jsx';
|
||||
import StrokeColorIndicatorComponent from '../../containers/stroke-color-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 styles from './paint-editor.css';
|
||||
|
@ -350,11 +350,14 @@ const PaintEditorComponent = props => {
|
|||
<EraserMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* Text mode will go here */}
|
||||
<LineMode
|
||||
<FillMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<FillMode
|
||||
<TextMode
|
||||
textArea={props.textArea}
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<LineMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<OvalMode
|
||||
|
@ -363,11 +366,10 @@ const PaintEditorComponent = props => {
|
|||
<RectMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* text tool, coming soon */}
|
||||
<TextModeComponent />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
{/* Canvas */}
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -383,6 +385,11 @@ const PaintEditorComponent = props => {
|
|||
svgId={props.svgId}
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<textarea
|
||||
className={styles.textArea}
|
||||
ref={props.setTextArea}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{props.isEyeDropping &&
|
||||
props.colorInfo !== null &&
|
||||
!props.colorInfo.hideLoupe ? (
|
||||
|
@ -394,6 +401,7 @@ const PaintEditorComponent = props => {
|
|||
</Box>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
<div className={styles.canvasControls}>
|
||||
<ComingSoonTooltip
|
||||
className={styles.bitmapTooltip}
|
||||
|
@ -459,7 +467,7 @@ const PaintEditorComponent = props => {
|
|||
PaintEditorComponent.propTypes = {
|
||||
canRedo: PropTypes.func.isRequired,
|
||||
canUndo: PropTypes.func.isRequired,
|
||||
canvas: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
canvas: PropTypes.instanceOf(Element),
|
||||
colorInfo: Loupe.propTypes.colorInfo,
|
||||
intl: intlShape,
|
||||
isEyeDropping: PropTypes.bool,
|
||||
|
@ -480,8 +488,10 @@ PaintEditorComponent.propTypes = {
|
|||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
setCanvas: PropTypes.func.isRequired,
|
||||
setTextArea: PropTypes.func.isRequired,
|
||||
svg: PropTypes.string,
|
||||
svgId: PropTypes.string
|
||||
svgId: PropTypes.string,
|
||||
textArea: PropTypes.instanceOf(Element)
|
||||
};
|
||||
|
||||
export default injectIntl(PaintEditorComponent);
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
|
||||
|
||||
import textIcon from './text.svg';
|
||||
|
||||
const TextModeComponent = () => (
|
||||
<ComingSoonTooltip
|
||||
place="right"
|
||||
tooltipId="text-mode-select"
|
||||
>
|
||||
const TextModeComponent = props => (
|
||||
<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
|
||||
isSelected={props.isSelected}
|
||||
onMouseDown={props.onMouseDown}
|
||||
/>
|
||||
</ComingSoonTooltip>
|
||||
);
|
||||
|
||||
TextModeComponent.propTypes = {
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onMouseDown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TextModeComponent;
|
||||
|
|
|
@ -30,7 +30,7 @@ class FillColorIndicator extends React.Component {
|
|||
}
|
||||
handleChangeFillColor (newColor) {
|
||||
// Apply color and update redux, but do not update svg until picker closes.
|
||||
const isDifferent = applyFillColorToSelection(newColor);
|
||||
const isDifferent = applyFillColorToSelection(newColor, this.props.textEditTarget);
|
||||
this._hasChanged = this._hasChanged || isDifferent;
|
||||
this.props.onChangeFillColor(newColor);
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ const mapStateToProps = state => ({
|
|||
disabled: state.scratchPaint.mode === Modes.LINE,
|
||||
fillColor: state.scratchPaint.color.fillColor,
|
||||
fillColorModalVisible: state.scratchPaint.modals.fillColor,
|
||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active
|
||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||
textEditTarget: state.scratchPaint.textEditTarget
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -76,7 +77,8 @@ FillColorIndicator.propTypes = {
|
|||
isEyeDropping: PropTypes.bool.isRequired,
|
||||
onChangeFillColor: PropTypes.func.isRequired,
|
||||
onCloseFillColor: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
textEditTarget: PropTypes.number
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -7,6 +7,8 @@ import {changeMode} from '../reducers/modes';
|
|||
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||
import {deactivateEyeDropper} from '../reducers/eye-dropper';
|
||||
import {setTextEditTarget} from '../reducers/text-edit-target';
|
||||
import {updateViewBounds} from '../reducers/view-bounds';
|
||||
|
||||
import {hideGuideLayers, showGuideLayers} from '../helper/layer';
|
||||
import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
||||
|
@ -36,10 +38,14 @@ class PaintEditor extends React.Component {
|
|||
'handleSendToFront',
|
||||
'handleGroup',
|
||||
'handleUngroup',
|
||||
'handleZoomIn',
|
||||
'handleZoomOut',
|
||||
'handleZoomReset',
|
||||
'canRedo',
|
||||
'canUndo',
|
||||
'onMouseDown',
|
||||
'setCanvas',
|
||||
'setTextArea',
|
||||
'startEyeDroppingLoop',
|
||||
'stopEyeDroppingLoop'
|
||||
]);
|
||||
|
@ -49,7 +55,14 @@ class PaintEditor extends React.Component {
|
|||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
document.addEventListener('keydown', this.props.onKeyPress);
|
||||
document.addEventListener('keydown', (/* event */) => {
|
||||
// Don't activate keyboard shortcuts during text editing
|
||||
if (!this.props.textEditing) {
|
||||
// @todo disabling keyboard shortcuts because there is a bug
|
||||
// that is interfering with text editing.
|
||||
// this.props.onKeyPress(event);
|
||||
}
|
||||
});
|
||||
// document listeners used to detect if a mouse is down outside of the
|
||||
// canvas, and should therefore stop the eye dropper
|
||||
document.addEventListener('mousedown', this.onMouseDown);
|
||||
|
@ -129,23 +142,37 @@ class PaintEditor extends React.Component {
|
|||
}
|
||||
handleZoomIn () {
|
||||
zoomOnSelection(PaintEditor.ZOOM_INCREMENT);
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
handleZoomOut () {
|
||||
zoomOnSelection(-PaintEditor.ZOOM_INCREMENT);
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
handleZoomReset () {
|
||||
resetZoom();
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
setCanvas (canvas) {
|
||||
this.setState({canvas: canvas});
|
||||
this.canvas = canvas;
|
||||
}
|
||||
setTextArea (element) {
|
||||
this.setState({textArea: element});
|
||||
}
|
||||
onMouseDown (event) {
|
||||
if (event.target === paper.view.element &&
|
||||
document.activeElement instanceof HTMLInputElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
if (event.target !== paper.view.element && event.target !== this.state.textArea) {
|
||||
// Exit text edit mode if you click anywhere outside of canvas
|
||||
this.props.removeTextEditTarget();
|
||||
}
|
||||
|
||||
if (this.props.isEyeDropping) {
|
||||
const colorString = this.eyeDropper.colorString;
|
||||
const callback = this.props.changeColorToEyeDropper;
|
||||
|
@ -209,8 +236,10 @@ class PaintEditor extends React.Component {
|
|||
rotationCenterX={this.props.rotationCenterX}
|
||||
rotationCenterY={this.props.rotationCenterY}
|
||||
setCanvas={this.setCanvas}
|
||||
setTextArea={this.setTextArea}
|
||||
svg={this.props.svg}
|
||||
svgId={this.props.svgId}
|
||||
textArea={this.state.textArea}
|
||||
onGroup={this.handleGroup}
|
||||
onRedo={this.handleRedo}
|
||||
onSendBackward={this.handleSendBackward}
|
||||
|
@ -244,16 +273,19 @@ PaintEditor.propTypes = {
|
|||
activate: PropTypes.func.isRequired,
|
||||
remove: PropTypes.func.isRequired
|
||||
}),
|
||||
removeTextEditTarget: PropTypes.func.isRequired,
|
||||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
setSelectedItems: PropTypes.func.isRequired,
|
||||
svg: PropTypes.string,
|
||||
svgId: PropTypes.string,
|
||||
textEditing: PropTypes.bool.isRequired,
|
||||
undoSnapshot: PropTypes.func.isRequired,
|
||||
undoState: PropTypes.shape({
|
||||
stack: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
pointer: PropTypes.number.isRequired
|
||||
})
|
||||
}),
|
||||
updateViewBounds: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -263,6 +295,7 @@ const mapStateToProps = state => ({
|
|||
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
|
||||
previousTool: state.scratchPaint.color.eyeDropper.previousTool,
|
||||
selectedItems: state.scratchPaint.selectedItems,
|
||||
textEditing: state.scratchPaint.textEditTarget !== null,
|
||||
undoState: state.scratchPaint.undo
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -275,14 +308,31 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(changeMode(Modes.LINE));
|
||||
} else if (event.key === 's') {
|
||||
dispatch(changeMode(Modes.SELECT));
|
||||
} else if (event.key === 'w') {
|
||||
dispatch(changeMode(Modes.RESHAPE));
|
||||
} else if (event.key === 'f') {
|
||||
dispatch(changeMode(Modes.FILL));
|
||||
} else if (event.key === 't') {
|
||||
dispatch(changeMode(Modes.TEXT));
|
||||
} else if (event.key === 'c') {
|
||||
dispatch(changeMode(Modes.OVAL));
|
||||
} else if (event.key === 'r') {
|
||||
dispatch(changeMode(Modes.RECT));
|
||||
}
|
||||
},
|
||||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
removeTextEditTarget: () => {
|
||||
dispatch(setTextEditTarget());
|
||||
},
|
||||
setSelectedItems: () => {
|
||||
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||
},
|
||||
onDeactivateEyeDropper: () => {
|
||||
// set redux values to default for eye dropper reducer
|
||||
dispatch(deactivateEyeDropper());
|
||||
},
|
||||
onUndo: () => {
|
||||
dispatch(undo());
|
||||
},
|
||||
|
@ -292,9 +342,8 @@ const mapDispatchToProps = dispatch => ({
|
|||
undoSnapshot: snapshot => {
|
||||
dispatch(undoSnapshot(snapshot));
|
||||
},
|
||||
onDeactivateEyeDropper: () => {
|
||||
// set redux values to default for eye dropper reducer
|
||||
dispatch(deactivateEyeDropper());
|
||||
updateViewBounds: matrix => {
|
||||
dispatch(updateViewBounds(matrix));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
width: 480px;
|
||||
height: 360px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
/* Turn off anti-aliasing for the drawing canvas. Each time it's updated it switches
|
||||
back and forth from aliased to unaliased and that looks bad */
|
||||
|
|
|
@ -16,7 +16,7 @@ import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view';
|
|||
import {ensureClockwise} from '../helper/math';
|
||||
import {clearHoveredItem} from '../reducers/hover';
|
||||
import {clearPasteOffset} from '../reducers/clipboard';
|
||||
|
||||
import {updateViewBounds} from '../reducers/view-bounds';
|
||||
|
||||
import styles from './paper-canvas.css';
|
||||
|
||||
|
@ -59,6 +59,7 @@ class PaperCanvas extends React.Component {
|
|||
const oldZoom = paper.project.view.zoom;
|
||||
const oldCenter = paper.project.view.center.clone();
|
||||
resetZoom();
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY);
|
||||
paper.project.view.zoom = oldZoom;
|
||||
paper.project.view.center = oldCenter;
|
||||
|
@ -85,13 +86,10 @@ class PaperCanvas extends React.Component {
|
|||
importSvg (svg, rotationCenterX, rotationCenterY) {
|
||||
const paperCanvas = this;
|
||||
// Pre-process SVG to prevent parsing errors (discussion from #213)
|
||||
// 1. Remove newlines and tab characters, chrome will not load urls with them.
|
||||
// https://www.chromestatus.com/feature/5735596811091968
|
||||
svg = svg.split(/[\n|\r|\t]/).join('');
|
||||
// 2. Remove svg: namespace on elements.
|
||||
// 1. Remove svg: namespace on elements.
|
||||
svg = svg.split(/<\s*svg:/).join('<');
|
||||
svg = svg.split(/<\/\s*svg:/).join('</');
|
||||
// 3. Add root svg namespace if it does not exist.
|
||||
// 2. Add root svg namespace if it does not exist.
|
||||
const svgAttrs = svg.match(/<svg [^>]*>/);
|
||||
if (svgAttrs && svgAttrs[0].indexOf('xmlns=') === -1) {
|
||||
svg = svg.replace(
|
||||
|
@ -179,16 +177,20 @@ class PaperCanvas extends React.Component {
|
|||
new paper.Point(offsetX, offsetY)
|
||||
);
|
||||
zoomOnFixedPoint(-event.deltaY / 100, fixedPoint);
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.props.setSelectedItems();
|
||||
} else if (event.shiftKey && event.deltaX === 0) {
|
||||
// Scroll horizontally (based on vertical scroll delta)
|
||||
// This is needed as for some browser/system combinations which do not set deltaX.
|
||||
// See #156.
|
||||
const dx = event.deltaY / paper.project.view.zoom;
|
||||
pan(dx, 0);
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
} else {
|
||||
const dx = event.deltaX / paper.project.view.zoom;
|
||||
const dy = event.deltaY / paper.project.view.zoom;
|
||||
pan(dx, dy);
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -218,7 +220,8 @@ PaperCanvas.propTypes = {
|
|||
setSelectedItems: PropTypes.func.isRequired,
|
||||
svg: PropTypes.string,
|
||||
svgId: PropTypes.string,
|
||||
undoSnapshot: PropTypes.func.isRequired
|
||||
undoSnapshot: PropTypes.func.isRequired,
|
||||
updateViewBounds: PropTypes.func.isRequired
|
||||
};
|
||||
const mapStateToProps = state => ({
|
||||
mode: state.scratchPaint.mode
|
||||
|
@ -241,6 +244,9 @@ const mapDispatchToProps = dispatch => ({
|
|||
},
|
||||
clearPasteOffset: () => {
|
||||
dispatch(clearPasteOffset());
|
||||
},
|
||||
updateViewBounds: matrix => {
|
||||
dispatch(updateViewBounds(matrix));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ class StrokeColorIndicator extends React.Component {
|
|||
}
|
||||
handleChangeStrokeColor (newColor) {
|
||||
// Apply color and update redux, but do not update svg until picker closes.
|
||||
const isDifferent = applyStrokeColorToSelection(newColor);
|
||||
const isDifferent = applyStrokeColorToSelection(newColor, this.props.textEditTarget);
|
||||
this._hasChanged = this._hasChanged || isDifferent;
|
||||
this.props.onChangeStrokeColor(newColor);
|
||||
}
|
||||
|
@ -51,10 +51,12 @@ class StrokeColorIndicator extends React.Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.scratchPaint.mode === Modes.BRUSH,
|
||||
disabled: state.scratchPaint.mode === Modes.BRUSH ||
|
||||
state.scratchPaint.mode === Modes.TEXT,
|
||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||
strokeColor: state.scratchPaint.color.strokeColor,
|
||||
strokeColorModalVisible: state.scratchPaint.modals.strokeColor
|
||||
strokeColorModalVisible: state.scratchPaint.modals.strokeColor,
|
||||
textEditTarget: state.scratchPaint.textEditTarget
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -76,7 +78,8 @@ StrokeColorIndicator.propTypes = {
|
|||
onCloseStrokeColor: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
strokeColor: PropTypes.string,
|
||||
strokeColorModalVisible: PropTypes.bool.isRequired
|
||||
strokeColorModalVisible: PropTypes.bool.isRequired,
|
||||
textEditTarget: PropTypes.number
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -15,7 +15,9 @@ class StrokeWidthIndicator extends React.Component {
|
|||
]);
|
||||
}
|
||||
handleChangeStrokeWidth (newWidth) {
|
||||
applyStrokeWidthToSelection(newWidth, this.props.onUpdateSvg);
|
||||
if (applyStrokeWidthToSelection(newWidth, this.props.textEditTarget)) {
|
||||
this.props.onUpdateSvg();
|
||||
}
|
||||
this.props.onChangeStrokeWidth(newWidth);
|
||||
}
|
||||
render () {
|
||||
|
@ -30,8 +32,10 @@ class StrokeWidthIndicator extends React.Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.scratchPaint.mode === Modes.BRUSH,
|
||||
strokeWidth: state.scratchPaint.color.strokeWidth
|
||||
disabled: state.scratchPaint.mode === Modes.BRUSH ||
|
||||
state.scratchPaint.mode === Modes.TEXT,
|
||||
strokeWidth: state.scratchPaint.color.strokeWidth,
|
||||
textEditTarget: state.scratchPaint.textEditTarget
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChangeStrokeWidth: strokeWidth => {
|
||||
|
@ -43,7 +47,8 @@ StrokeWidthIndicator.propTypes = {
|
|||
disabled: PropTypes.bool.isRequired,
|
||||
onChangeStrokeWidth: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
strokeWidth: PropTypes.number
|
||||
strokeWidth: PropTypes.number,
|
||||
textEditTarget: PropTypes.number
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
148
src/containers/text-mode.jsx
Normal file
148
src/containers/text-mode.jsx
Normal file
|
@ -0,0 +1,148 @@
|
|||
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 {setTextEditTarget} from '../reducers/text-edit-target';
|
||||
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 (this.tool && !nextProps.textEditTarget && this.props.textEditTarget) {
|
||||
this.tool.onTextEditCancelled();
|
||||
}
|
||||
if (this.tool && !nextProps.viewBounds.equals(this.props.viewBounds)) {
|
||||
this.tool.onViewBoundsChanged(nextProps.viewBounds);
|
||||
}
|
||||
|
||||
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.textArea,
|
||||
this.props.setSelectedItems,
|
||||
this.props.clearSelectedItems,
|
||||
this.props.onUpdateSvg,
|
||||
this.props.setTextEditTarget,
|
||||
);
|
||||
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,
|
||||
setTextEditTarget: PropTypes.func.isRequired,
|
||||
textArea: PropTypes.instanceOf(Element),
|
||||
textEditTarget: PropTypes.number,
|
||||
viewBounds: PropTypes.instanceOf(paper.Matrix).isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
colorState: state.scratchPaint.color,
|
||||
isTextModeActive: state.scratchPaint.mode === Modes.TEXT,
|
||||
selectedItems: state.scratchPaint.selectedItems,
|
||||
textEditTarget: state.scratchPaint.textEditTarget,
|
||||
viewBounds: state.scratchPaint.viewBounds
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
setSelectedItems: () => {
|
||||
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||
},
|
||||
setTextEditTarget: targetId => {
|
||||
dispatch(setTextEditTarget(targetId));
|
||||
},
|
||||
handleMouseDown: () => {
|
||||
dispatch(changeMode(Modes.TEXT));
|
||||
},
|
||||
onChangeFillColor: fillColor => {
|
||||
dispatch(changeFillColor(fillColor));
|
||||
},
|
||||
onChangeStrokeColor: strokeColor => {
|
||||
dispatch(changeStrokeColor(strokeColor));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TextMode);
|
|
@ -30,14 +30,19 @@ const hoverItem = function (item) {
|
|||
return clone;
|
||||
};
|
||||
|
||||
const hoverBounds = function (item) {
|
||||
const rect = new paper.Path.Rectangle(item.internalBounds);
|
||||
const hoverBounds = function (item, expandBy) {
|
||||
let bounds = item.internalBounds;
|
||||
if (expandBy) {
|
||||
bounds = bounds.expand(expandBy);
|
||||
}
|
||||
const rect = new paper.Path.Rectangle(bounds);
|
||||
rect.matrix = item.matrix;
|
||||
setDefaultGuideStyle(rect);
|
||||
rect.parent = getGuideLayer();
|
||||
rect.strokeColor = GUIDE_BLUE;
|
||||
rect.fillColor = null;
|
||||
rect.data.isHelperItem = true;
|
||||
rect.data.origItem = item;
|
||||
rect.bringToFront();
|
||||
|
||||
return rect;
|
||||
|
@ -104,7 +109,8 @@ const removeHitPoint = function () {
|
|||
const drawHitPoint = function (point) {
|
||||
removeHitPoint();
|
||||
if (point) {
|
||||
const hitPoint = paper.Path.Circle(point, 4 /* radius */);
|
||||
const hitPoint = paper.Path.Circle(point, 4 / paper.view.zoom /* radius */);
|
||||
hitPoint.strokeWidth = 1 / paper.view.zoom;
|
||||
hitPoint.strokeColor = GUIDE_BLUE;
|
||||
hitPoint.fillColor = new paper.Color(1, 1, 1, 0.5);
|
||||
hitPoint.parent = getGuideLayer();
|
||||
|
|
|
@ -2,6 +2,7 @@ import paper from '@scratch/paper';
|
|||
import {getSelectedLeafItems} from './selection';
|
||||
import {isPGTextItem, isPointTextItem} from './item';
|
||||
import {isGroup} from './group';
|
||||
import {getItems} from './selection';
|
||||
|
||||
const MIXED = 'scratch-paint/style-path/mixed';
|
||||
|
||||
|
@ -15,56 +16,51 @@ const _colorMatch = function (itemColor, incomingColor) {
|
|||
(itemColor && incomingColor && itemColor.toCSS() === new paper.Color(incomingColor).toCSS());
|
||||
};
|
||||
|
||||
// Selected items and currently active text edit items respond to color changes.
|
||||
const _getColorStateListeners = function (textEditTargetId) {
|
||||
const items = getSelectedLeafItems();
|
||||
if (textEditTargetId) {
|
||||
const matches = getItems({
|
||||
match: item => item.id === textEditTargetId
|
||||
});
|
||||
if (matches.length) {
|
||||
items.push(matches[0]);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when setting fill color
|
||||
* @param {string} colorString New color, css format
|
||||
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
|
||||
* @return {boolean} Whether the color application actually changed visibly.
|
||||
*/
|
||||
const applyFillColorToSelection = function (colorString) {
|
||||
const items = getSelectedLeafItems();
|
||||
const applyFillColorToSelection = function (colorString, textEditTargetId) {
|
||||
const items = _getColorStateListeners(textEditTargetId);
|
||||
let changed = false;
|
||||
for (let item of items) {
|
||||
if (item.parent instanceof paper.CompoundPath) {
|
||||
item = item.parent;
|
||||
}
|
||||
if (isPGTextItem(item)) {
|
||||
for (const child of item.children) {
|
||||
if (child.children) {
|
||||
for (const path of child.children) {
|
||||
if (!path.data.isPGGlyphRect) {
|
||||
if (!_colorMatch(path.fillColor, colorString)) {
|
||||
changed = true;
|
||||
path.fillColor = colorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!child.data.isPGGlyphRect) {
|
||||
if (!_colorMatch(child.fillColor, colorString)) {
|
||||
changed = true;
|
||||
child.fillColor = colorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isPointTextItem(item) && !colorString) {
|
||||
colorString = 'rgba(0,0,0,0)';
|
||||
} else if (item.parent instanceof paper.CompoundPath) {
|
||||
item = item.parent;
|
||||
}
|
||||
if (!_colorMatch(item.fillColor, colorString)) {
|
||||
changed = true;
|
||||
item.fillColor = colorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when setting stroke color
|
||||
* @param {string} colorString New color, css format
|
||||
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
|
||||
* @return {boolean} Whether the color application actually changed visibly.
|
||||
*/
|
||||
const applyStrokeColorToSelection = function (colorString) {
|
||||
const items = getSelectedLeafItems();
|
||||
const applyStrokeColorToSelection = function (colorString, textEditTargetId) {
|
||||
const items = _getColorStateListeners(textEditTargetId);
|
||||
let changed = false;
|
||||
for (let item of items) {
|
||||
if (item.parent instanceof paper.CompoundPath) {
|
||||
|
@ -106,11 +102,12 @@ const applyStrokeColorToSelection = function (colorString) {
|
|||
/**
|
||||
* Called when setting stroke width
|
||||
* @param {number} value New stroke width
|
||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
|
||||
* @return {boolean} Whether the color application actually changed visibly.
|
||||
*/
|
||||
const applyStrokeWidthToSelection = function (value, onUpdateSvg) {
|
||||
const applyStrokeWidthToSelection = function (value, textEditTargetId) {
|
||||
let changed = false;
|
||||
const items = getSelectedLeafItems();
|
||||
const items = _getColorStateListeners(textEditTargetId);
|
||||
for (let item of items) {
|
||||
if (item.parent instanceof paper.CompoundPath) {
|
||||
item = item.parent;
|
||||
|
@ -122,9 +119,7 @@ const applyStrokeWidthToSelection = function (value, onUpdateSvg) {
|
|||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
onUpdateSvg();
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -38,14 +38,14 @@ class FillTool extends paper.Tool {
|
|||
item.lastSegment.point.getDistance(item.firstSegment.point) < 8;
|
||||
};
|
||||
return {
|
||||
class: paper.Path,
|
||||
segments: true,
|
||||
stroke: true,
|
||||
curves: true,
|
||||
fill: true,
|
||||
guide: false,
|
||||
match: function (hitResult) {
|
||||
return (hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item));
|
||||
return (hitResult.item instanceof paper.Path || hitResult.item instanceof paper.PointText) &&
|
||||
(hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item));
|
||||
},
|
||||
hitUnfilledPaths: true,
|
||||
tolerance: FillTool.TOLERANCE / paper.view.zoom
|
||||
|
|
326
src/helper/tools/text-tool.js
Normal file
326
src/helper/tools/text-tool.js
Normal file
|
@ -0,0 +1,326 @@
|
|||
import paper from '@scratch/paper';
|
||||
import Modes from '../../lib/modes';
|
||||
import {clearSelection} from '../selection';
|
||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||
import NudgeTool from '../selection-tools/nudge-tool';
|
||||
import {hoverBounds} from '../guides';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
static get TEXT_EDIT_MODE () {
|
||||
return 'TEXT_EDIT_MODE';
|
||||
}
|
||||
static get SELECT_MODE () {
|
||||
return 'SELECT_MODE';
|
||||
}
|
||||
/** Clicks registered within this amount of time are registered as double clicks */
|
||||
static get DOUBLE_CLICK_MILLIS () {
|
||||
return 250;
|
||||
}
|
||||
/** Typing with no pauses longer than this amount of type will count as 1 action */
|
||||
static get TYPING_TIMEOUT_MILLIS () {
|
||||
return 1000;
|
||||
}
|
||||
static get TEXT_PADDING () {
|
||||
return 8;
|
||||
}
|
||||
/**
|
||||
* @param {HTMLTextAreaElement} textAreaElement dom element for the editable text field
|
||||
* @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
|
||||
* @param {!function} setTextEditTarget Call to set text editing target whenever text editing is active
|
||||
*/
|
||||
constructor (textAreaElement, setSelectedItems, clearSelectedItems, onUpdateSvg, setTextEditTarget) {
|
||||
super();
|
||||
this.element = textAreaElement;
|
||||
this.setSelectedItems = setSelectedItems;
|
||||
this.clearSelectedItems = clearSelectedItems;
|
||||
this.onUpdateSvg = onUpdateSvg;
|
||||
this.setTextEditTarget = setTextEditTarget;
|
||||
this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateSvg);
|
||||
this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg);
|
||||
this.lastEvent = null;
|
||||
|
||||
// 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.onMouseMove = this.handleMouseMove;
|
||||
this.onKeyUp = this.handleKeyUp;
|
||||
this.onKeyDown = this.handleKeyDown;
|
||||
|
||||
this.textBox = null;
|
||||
this.guide = null;
|
||||
this.colorState = null;
|
||||
this.mode = null;
|
||||
this.active = false;
|
||||
this.lastTypeEvent = 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
|
||||
}
|
||||
getBoundingBoxHitOptions () {
|
||||
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
|
||||
};
|
||||
}
|
||||
getTextEditHitOptions () {
|
||||
return {
|
||||
class: paper.PointText,
|
||||
segments: true,
|
||||
stroke: true,
|
||||
curves: true,
|
||||
fill: true,
|
||||
guide: false,
|
||||
match: hitResult => hitResult.item && !hitResult.item.selected, // Unselected only
|
||||
tolerance: TextTool.TOLERANCE / paper.view.zoom
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Called when 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);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called when the view matrix changes
|
||||
* @param {paper.Matrix} viewMtx applied to paper.view
|
||||
*/
|
||||
onViewBoundsChanged (viewMtx) {
|
||||
if (this.mode !== TextTool.TEXT_EDIT_MODE) {
|
||||
return;
|
||||
}
|
||||
const matrix = this.textBox.matrix;
|
||||
this.element.style.transform =
|
||||
`translate(0px, ${this.textBox.internalBounds.y}px)
|
||||
matrix(${viewMtx.a}, ${viewMtx.b}, ${viewMtx.c}, ${viewMtx.d},
|
||||
${viewMtx.tx}, ${viewMtx.ty})
|
||||
matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d},
|
||||
${matrix.tx}, ${matrix.ty})`;
|
||||
}
|
||||
setColorState (colorState) {
|
||||
this.colorState = colorState;
|
||||
}
|
||||
handleMouseMove (event) {
|
||||
const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions());
|
||||
if (hitResults.length) {
|
||||
document.body.style.cursor = 'text';
|
||||
} else {
|
||||
document.body.style.cursor = 'auto';
|
||||
}
|
||||
}
|
||||
handleMouseDown (event) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
this.lastEvent = event;
|
||||
|
||||
const doubleClickHitTest = paper.project.hitTest(event.point, this.getBoundingBoxHitOptions());
|
||||
if (doubleClicked &&
|
||||
this.mode === TextTool.SELECT_MODE &&
|
||||
doubleClickHitTest) {
|
||||
// Double click in select mode moves you to text edit mode
|
||||
clearSelection(this.clearSelectedItems);
|
||||
this.textBox = doubleClickHitTest.item;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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();
|
||||
}
|
||||
} else {
|
||||
// In no mode or select mode clicking away to begin text edit mode
|
||||
this.textBox = new paper.PointText({
|
||||
point: event.point,
|
||||
content: '',
|
||||
font: 'Helvetica',
|
||||
fontSize: 30,
|
||||
fillColor: this.colorState.fillColor,
|
||||
// Default leading for both the HTML text area and paper.PointText
|
||||
// is 120%, but for some reason they are slightly off from each other.
|
||||
// This value was obtained experimentally.
|
||||
// (Don't round to 34.6, the text area will start to scroll.)
|
||||
leading: 34.61
|
||||
});
|
||||
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
|
||||
}
|
||||
}
|
||||
handleMouseDrag (event) {
|
||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
|
||||
if (this.mode === TextTool.SELECT_MODE) {
|
||||
this.boundingBoxTool.onMouseDrag(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
this.active = false;
|
||||
}
|
||||
handleKeyUp (event) {
|
||||
if (this.mode === TextTool.SELECT_MODE) {
|
||||
this.nudgeTool.onKeyUp(event);
|
||||
}
|
||||
}
|
||||
handleKeyDown (event) {
|
||||
if (event.event.target instanceof HTMLInputElement) {
|
||||
// Ignore nudge if a text input field is focused
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mode === TextTool.SELECT_MODE) {
|
||||
this.nudgeTool.onKeyUp(event);
|
||||
}
|
||||
}
|
||||
handleTextInput (event) {
|
||||
// Save undo state if you paused typing for long enough.
|
||||
if (this.lastTypeEvent && event.timeStamp - this.lastTypeEvent.timeStamp > TextTool.TYPING_TIMEOUT_MILLIS) {
|
||||
this.onUpdateSvg();
|
||||
}
|
||||
this.lastTypeEvent = event;
|
||||
if (this.mode === TextTool.TEXT_EDIT_MODE) {
|
||||
this.textBox.content = this.element.value;
|
||||
}
|
||||
this.resizeGuide();
|
||||
}
|
||||
resizeGuide () {
|
||||
if (this.guide) this.guide.remove();
|
||||
this.guide = hoverBounds(this.textBox, TextTool.TEXT_PADDING);
|
||||
this.guide.dashArray = [4, 4];
|
||||
this.element.style.width = `${this.textBox.internalBounds.width}px`;
|
||||
this.element.style.height = `${this.textBox.internalBounds.height}px`;
|
||||
}
|
||||
/**
|
||||
* @param {string} initialText Text to initialize the text area with
|
||||
* @param {paper.Matrix} matrix Transform matrix for the element. Defaults
|
||||
* to the identity matrix.
|
||||
*/
|
||||
beginTextEdit (initialText, matrix) {
|
||||
this.mode = TextTool.TEXT_EDIT_MODE;
|
||||
this.setTextEditTarget(this.textBox.id);
|
||||
|
||||
const viewMtx = paper.view.matrix;
|
||||
|
||||
this.element.style.display = 'initial';
|
||||
this.element.value = initialText ? initialText : '';
|
||||
this.element.style.transformOrigin =
|
||||
`${-this.textBox.internalBounds.x}px ${-this.textBox.internalBounds.y}px`;
|
||||
this.element.style.transform =
|
||||
`translate(0px, ${this.textBox.internalBounds.y}px)
|
||||
matrix(${viewMtx.a}, ${viewMtx.b}, ${viewMtx.c}, ${viewMtx.d},
|
||||
${viewMtx.tx}, ${viewMtx.ty})
|
||||
matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d},
|
||||
${matrix.tx}, ${matrix.ty})`;
|
||||
this.element.focus({preventScroll: true});
|
||||
this.eventListener = this.handleTextInput.bind(this);
|
||||
this.element.addEventListener('input', this.eventListener);
|
||||
this.resizeGuide();
|
||||
}
|
||||
endTextEdit () {
|
||||
if (this.mode !== TextTool.TEXT_EDIT_MODE) {
|
||||
return;
|
||||
}
|
||||
this.mode = null;
|
||||
|
||||
// Remove invisible textboxes
|
||||
if (this.textBox && this.textBox.content.trim() === '') {
|
||||
this.textBox.remove();
|
||||
this.textBox = null;
|
||||
}
|
||||
|
||||
// Remove guide
|
||||
if (this.guide) {
|
||||
this.guide.remove();
|
||||
this.guide = null;
|
||||
this.setTextEditTarget();
|
||||
}
|
||||
this.element.style.display = 'none';
|
||||
if (this.eventListener) {
|
||||
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) {
|
||||
this.onUpdateSvg();
|
||||
}
|
||||
}
|
||||
deactivateTool () {
|
||||
if (this.textBox && this.textBox.content.trim() === '') {
|
||||
this.textBox.remove();
|
||||
this.textBox = null;
|
||||
}
|
||||
this.endTextEdit();
|
||||
this.boundingBoxTool.removeBoundsPath();
|
||||
}
|
||||
}
|
||||
|
||||
export default TextTool;
|
|
@ -9,7 +9,8 @@ const Modes = keyMirror({
|
|||
RESHAPE: null,
|
||||
OVAL: null,
|
||||
RECT: null,
|
||||
ROUNDED_RECT: null
|
||||
ROUNDED_RECT: null,
|
||||
TEXT: null
|
||||
});
|
||||
|
||||
export default Modes;
|
||||
|
|
|
@ -7,6 +7,8 @@ import clipboardReducer from './clipboard';
|
|||
import hoverReducer from './hover';
|
||||
import modalsReducer from './modals';
|
||||
import selectedItemReducer from './selected-items';
|
||||
import textEditTargetReducer from './text-edit-target';
|
||||
import viewBoundsReducer from './view-bounds';
|
||||
import undoReducer from './undo';
|
||||
|
||||
export default combineReducers({
|
||||
|
@ -18,5 +20,7 @@ export default combineReducers({
|
|||
hoveredItemId: hoverReducer,
|
||||
modals: modalsReducer,
|
||||
selectedItems: selectedItemReducer,
|
||||
undo: undoReducer
|
||||
textEditTarget: textEditTargetReducer,
|
||||
undo: undoReducer,
|
||||
viewBounds: viewBoundsReducer
|
||||
});
|
||||
|
|
40
src/reducers/text-edit-target.js
Normal file
40
src/reducers/text-edit-target.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import log from '../log/log';
|
||||
|
||||
const CHANGE_TEXT_EDIT_TARGET = 'scratch-paint/text-tool/CHANGE_TEXT_EDIT_TARGET';
|
||||
const initialState = null;
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case CHANGE_TEXT_EDIT_TARGET:
|
||||
if (typeof action.textEditTargetId === 'undefined') {
|
||||
log.warn(`Text edit target should not be set to undefined. Use null.`);
|
||||
return state;
|
||||
} else if (typeof action.textEditTargetId === 'undefined' || isNaN(action.textEditTargetId)) {
|
||||
log.warn(`Text edit target should be an item ID number. Got: ${action.textEditTargetId}`);
|
||||
return state;
|
||||
}
|
||||
return action.textEditTargetId;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Action creators ==================================
|
||||
/**
|
||||
* Set the currently-being-edited text field to the given item ID
|
||||
* @param {?number} textEditTargetId The paper.Item ID of the active text field.
|
||||
* Leave empty if there is no text editing target.
|
||||
* @return {object} Redux action to change the text edit target.
|
||||
*/
|
||||
const setTextEditTarget = function (textEditTargetId) {
|
||||
return {
|
||||
type: CHANGE_TEXT_EDIT_TARGET,
|
||||
textEditTargetId: textEditTargetId ? textEditTargetId : null
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
setTextEditTarget
|
||||
};
|
37
src/reducers/view-bounds.js
Normal file
37
src/reducers/view-bounds.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import paper from '@scratch/paper';
|
||||
import log from '../log/log';
|
||||
|
||||
const UPDATE_VIEW_BOUNDS = 'scratch-paint/view/UPDATE_VIEW_BOUNDS';
|
||||
const initialState = new paper.Matrix(); // Identity
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case UPDATE_VIEW_BOUNDS:
|
||||
if (!(action.viewBounds instanceof paper.Matrix)) {
|
||||
log.warn(`View bounds should be a paper.Matrix.`);
|
||||
return state;
|
||||
}
|
||||
return action.viewBounds;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Action creators ==================================
|
||||
/**
|
||||
* Set the view bounds, which defines the zoom and scroll of the paper canvas.
|
||||
* @param {paper.Matrix} matrix The matrix applied to the view
|
||||
* @return {object} Redux action to set the view bounds
|
||||
*/
|
||||
const updateViewBounds = function (matrix) {
|
||||
return {
|
||||
type: UPDATE_VIEW_BOUNDS,
|
||||
viewBounds: matrix.clone()
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
updateViewBounds
|
||||
};
|
Loading…
Reference in a new issue