From 9a5a273f5bf8b51c060398a27188363c71c639a4 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 4 Apr 2018 14:31:27 -0400 Subject: [PATCH 01/17] drawing some dots --- src/helper/blob-tools/broad-brush-helper.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/helper/blob-tools/broad-brush-helper.js b/src/helper/blob-tools/broad-brush-helper.js index dbd81eb6..17666511 100644 --- a/src/helper/blob-tools/broad-brush-helper.js +++ b/src/helper/blob-tools/broad-brush-helper.js @@ -2,6 +2,7 @@ import paper from '@scratch/paper'; import {styleBlob} from '../../helper/style-path'; import log from '../../log/log'; +import {getRaster} from '../../helper/layer'; /** * Broad brush functions to add as listeners on the mouse. Call them when the corresponding mouse event happens @@ -44,6 +45,7 @@ class BroadBrushHelper { }); styleBlob(this.finalPath, options); this.lastPoint = event.point; + getRaster().setPixel(event.point, 'blue'); } onBroadMouseDrag (event, tool, options) { From 31561d8bdab2953e1cafc2e9dc7f867d91fc4ab8 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 4 Apr 2018 17:37:11 -0400 Subject: [PATCH 02/17] Add bitmap brush tool button --- .../bit-brush-mode/bit-brush-mode.jsx | 25 ++ src/components/paint-editor/paint-editor.jsx | 4 + src/containers/bit-brush-mode.jsx | 111 ++++++ src/helper/bit-tools/brush-tool.js | 50 +++ src/helper/bit-tools/fill-tool.js | 177 ++++++++++ src/helper/bit-tools/oval-tool.js | 133 +++++++ src/helper/bit-tools/rect-tool.js | 127 +++++++ src/helper/bit-tools/text-tool.js | 326 ++++++++++++++++++ src/helper/blob-tools/broad-brush-helper.js | 2 - src/lib/modes.js | 1 + 10 files changed, 954 insertions(+), 2 deletions(-) create mode 100644 src/components/bit-brush-mode/bit-brush-mode.jsx create mode 100644 src/containers/bit-brush-mode.jsx create mode 100644 src/helper/bit-tools/brush-tool.js create mode 100644 src/helper/bit-tools/fill-tool.js create mode 100644 src/helper/bit-tools/oval-tool.js create mode 100644 src/helper/bit-tools/rect-tool.js create mode 100644 src/helper/bit-tools/text-tool.js diff --git a/src/components/bit-brush-mode/bit-brush-mode.jsx b/src/components/bit-brush-mode/bit-brush-mode.jsx new file mode 100644 index 00000000..1535d983 --- /dev/null +++ b/src/components/bit-brush-mode/bit-brush-mode.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx'; + +import brushIcon from '../brush-mode/brush.svg'; // @todo: replace + +const BitBrushModeComponent = props => ( + +); + +BitBrushModeComponent.propTypes = { + isSelected: PropTypes.bool.isRequired, + onMouseDown: PropTypes.func.isRequired +}; + +export default BitBrushModeComponent; diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 771bf79e..38bca1aa 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -10,6 +10,7 @@ import PaperCanvas from '../../containers/paper-canvas.jsx'; import {shouldShowGroup, shouldShowUngroup} from '../../helper/group'; import {shouldShowBringForward, shouldShowSendBackward} from '../../helper/order'; +import BitBrushMode from '../../containers/bit-brush-mode.jsx'; import Box from '../box/box.jsx'; import Button from '../button/button.jsx'; import ButtonGroup from '../button-group/button-group.jsx'; @@ -372,6 +373,9 @@ const PaintEditorComponent = props => { + ) : null} diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx new file mode 100644 index 00000000..183a72d0 --- /dev/null +++ b/src/containers/bit-brush-mode.jsx @@ -0,0 +1,111 @@ +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 {changeBrushSize} from '../reducers/brush-mode'; +import {changeMode} from '../reducers/modes'; +import {clearSelectedItems} from '../reducers/selected-items'; +import {clearSelection} from '../helper/selection'; + +import BitBrushModeComponent from '../components/bit-brush-mode/bit-brush-mode.jsx'; +import BitBrushTool from '../helper/bit-tools/brush-tool'; + +class BitBrushMode extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool' + ]); + } + componentDidMount () { + if (this.props.isBitBrushModeActive) { + this.activateTool(this.props); + } + } + componentWillReceiveProps (nextProps) { + if (this.tool && nextProps.colorState !== this.props.colorState) { + this.tool.setColorState(nextProps.colorState); + } + + if (nextProps.isBitBrushModeActive && !this.props.isBitBrushModeActive) { + this.activateTool(); + } else if (!nextProps.isBitBrushModeActive && this.props.isBitBrushModeActive) { + this.deactivateTool(); + } + } + shouldComponentUpdate (nextProps) { + return nextProps.isBrushModeActive !== this.props.isBitBrushModeActive; + } + activateTool () { + clearSelection(this.props.clearSelectedItems); + // Force the default brush color if fill is MIXED or transparent + const {fillColor} = this.props.colorState; + if (fillColor === MIXED || fillColor === null) { + this.props.onChangeFillColor(DEFAULT_COLOR); + } + this.tool = new BitBrushTool( + this.props.onUpdateSvg + ); + this.tool.setColorState(this.props.colorState); + this.tool.activate(); + } + deactivateTool () { + this.tool.deactivateTool(); + this.tool.remove(); + this.tool = null; + } + render () { + return ( + + ); + } +} + +BitBrushMode.propTypes = { + bitBrushModeState: PropTypes.shape({ + brushSize: PropTypes.number.isRequired + }), + clearSelectedItems: PropTypes.func.isRequired, + colorState: PropTypes.shape({ + fillColor: PropTypes.string, + strokeColor: PropTypes.string, + strokeWidth: PropTypes.number + }).isRequired, + handleMouseDown: PropTypes.func.isRequired, + isBitBrushModeActive: PropTypes.bool.isRequired, + onChangeFillColor: PropTypes.func.isRequired, + onUpdateSvg: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + brushModeState: state.scratchPaint.brushMode, + colorState: state.scratchPaint.color, + isBitBrushModeActive: state.scratchPaint.mode === Modes.BIT_BRUSH +}); +const mapDispatchToProps = dispatch => ({ + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, + changeBrushSize: brushSize => { + dispatch(changeBrushSize(brushSize)); + }, + handleMouseDown: () => { + dispatch(changeMode(Modes.BIT_BRUSH)); + }, + onChangeFillColor: fillColor => { + dispatch(changeFillColor(fillColor)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BitBrushMode); diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js new file mode 100644 index 00000000..6ec3ca39 --- /dev/null +++ b/src/helper/bit-tools/brush-tool.js @@ -0,0 +1,50 @@ +import paper from '@scratch/paper'; +import {getRaster} from '../layer'; + +/** + * Tool for drawing with the bitmap brush. + */ +class BrushTool extends paper.Tool { + /** + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (onUpdateSvg) { + super(); + this.onUpdateSvg = onUpdateSvg; + + // 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.colorState = null; + this.active = false; + } + setColorState (colorState) { + this.colorState = colorState; + } + handleMouseDown (event) { + if (event.event.button > 0) return; // only first mouse button + this.active = true; + getRaster().setPixel(event.point, 'blue'); + } + handleMouseDrag (event) { + if (event.event.button > 0 || !this.active) return; // only first mouse button + + if (this.isBoundingBoxMode) { + this.boundingBoxTool.onMouseDrag(event); + return; + } + getRaster().setPixel(event.point, 'blue'); + } + handleMouseUp (event) { + if (event.event.button > 0 || !this.active) return; // only first mouse button + getRaster().setPixel(event.point, 'blue'); + this.active = false; + } + deactivateTool () { + } +} + +export default BrushTool; diff --git a/src/helper/bit-tools/fill-tool.js b/src/helper/bit-tools/fill-tool.js new file mode 100644 index 00000000..9b43ff9f --- /dev/null +++ b/src/helper/bit-tools/fill-tool.js @@ -0,0 +1,177 @@ +import paper from '@scratch/paper'; +import {getHoveredItem} from '../hover'; +import {expandBy} from '../math'; + +class FillTool extends paper.Tool { + static get TOLERANCE () { + return 2; + } + /** + * @param {function} setHoveredItem Callback to set the hovered item + * @param {function} clearHoveredItem Callback to clear the hovered item + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ + constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) { + super(); + this.setHoveredItem = setHoveredItem; + this.clearHoveredItem = clearHoveredItem; + this.onUpdateSvg = onUpdateSvg; + + // We have to set these functions instead of just declaring them because + // paper.js tools hook up the listeners in the setter functions. + this.onMouseMove = this.handleMouseMove; + this.onMouseUp = this.handleMouseUp; + + // Color to fill with + this.fillColor = null; + // The path that's being hovered over. + this.fillItem = null; + // If we're hovering over a hole in a compound path, we can't just recolor it. This is the + // added item that's the same shape as the hole that's drawn over the hole when we fill a hole. + this.addedFillItem = null; + this.fillItemOrigColor = null; + this.prevHoveredItemId = null; + } + getHitOptions () { + const isAlmostClosedPath = function (item) { + return item instanceof paper.Path && item.segments.length > 2 && + item.lastSegment.point.getDistance(item.firstSegment.point) < 8; + }; + return { + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false, + match: function (hitResult) { + 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 + }; + } + setFillColor (fillColor) { + this.fillColor = fillColor; + } + /** + * To be called when the hovered item changes. When the select tool hovers over a + * new item, it compares against this to see if a hover item change event needs to + * be fired. + * @param {paper.Item} prevHoveredItemId ID of the highlight item that indicates the mouse is + * over a given item currently + */ + setPrevHoveredItemId (prevHoveredItemId) { + this.prevHoveredItemId = prevHoveredItemId; + } + handleMouseMove (event) { + const hoveredItem = getHoveredItem(event, this.getHitOptions(), true /* subselect */); + if ((!hoveredItem && this.prevHoveredItemId) || // There is no longer a hovered item + (hoveredItem && !this.prevHoveredItemId) || // There is now a hovered item + (hoveredItem && this.prevHoveredItemId && + hoveredItem.id !== this.prevHoveredItemId)) { // hovered item changed + this.setHoveredItem(hoveredItem ? hoveredItem.id : null); + } + const hitItem = hoveredItem ? hoveredItem.data.origItem : null; + // Still hitting the same thing + if ((!hitItem && !this.fillItem) || this.fillItem === hitItem) { + return; + } + if (this.fillItem) { + if (this.addedFillItem) { + this.addedFillItem.remove(); + this.addedFillItem = null; + } else { + this._setFillItemColor(this.fillItemOrigColor); + } + this.fillItemOrigColor = null; + this.fillItem = null; + } + if (hitItem) { + this.fillItem = hitItem; + this.fillItemOrigColor = hitItem.fillColor; + if (hitItem.parent instanceof paper.CompoundPath && hitItem.area < 0) { // hole + if (!this.fillColor) { + // Hole filled with transparent is no-op + this.fillItem = null; + this.fillItemOrigColor = null; + return; + } + // Make an item to fill the hole + this.addedFillItem = hitItem.clone(); + this.addedFillItem.setClockwise(true); + this.addedFillItem.data.noHover = true; + this.addedFillItem.data.origItem = hitItem; + // This usually fixes it so there isn't a teeny tiny gap in between the fill and the outline + // when filling in a hole + expandBy(this.addedFillItem, .1); + this.addedFillItem.insertAbove(hitItem.parent); + } else if (this.fillItem.parent instanceof paper.CompoundPath) { + this.fillItemOrigColor = hitItem.parent.fillColor; + } + this._setFillItemColor(this.fillColor); + } + } + handleMouseUp (event) { + if (event.event.button > 0) return; // only first mouse button + if (this.fillItem) { + // If the hole we're filling in is the same color as the parent, and parent has no outline, remove the hole + if (this.addedFillItem && + this._noStroke(this.fillItem.parent) && + this.addedFillItem.fillColor.type !== 'gradient' && + this.fillItem.parent.fillColor.toCSS() === this.addedFillItem.fillColor.toCSS()) { + this.addedFillItem.remove(); + this.addedFillItem = null; + let parent = this.fillItem.parent; + this.fillItem.remove(); + parent = parent.reduce(); + parent.fillColor = this.fillColor; + } else if (this.addedFillItem) { + // Fill in a hole. + this.addedFillItem.data.noHover = false; + } else if (!this.fillColor && + this.fillItem.data && + this.fillItem.data.origItem) { + // Filling a hole filler with transparent returns it to being gone + // instead of making a shape that's transparent + const group = this.fillItem.parent; + this.fillItem.remove(); + if (!(group instanceof paper.Layer) && group.children.length === 1) { + group.reduce(); + } + } + + this.clearHoveredItem(); + this.fillItem = null; + this.addedFillItem = null; + this.fillItemOrigColor = null; + this.onUpdateSvg(); + } + } + _noStroke (item) { + return !item.strokeColor || + item.strokeColor.alpha === 0 || + item.strokeWidth === 0; + } + _setFillItemColor (color) { + if (this.addedFillItem) { + this.addedFillItem.fillColor = color; + } else if (this.fillItem.parent instanceof paper.CompoundPath) { + this.fillItem.parent.fillColor = color; + } else { + this.fillItem.fillColor = color; + } + } + deactivateTool () { + if (this.fillItem) { + this._setFillItemColor(this.fillItemOrigColor); + this.fillItemOrigColor = null; + this.fillItem = null; + } + this.clearHoveredItem(); + this.setHoveredItem = null; + this.clearHoveredItem = null; + } +} + +export default FillTool; diff --git a/src/helper/bit-tools/oval-tool.js b/src/helper/bit-tools/oval-tool.js new file mode 100644 index 00000000..dc90006e --- /dev/null +++ b/src/helper/bit-tools/oval-tool.js @@ -0,0 +1,133 @@ +import paper from '@scratch/paper'; +import Modes from '../../lib/modes'; +import {styleShape} from '../style-path'; +import {clearSelection} from '../selection'; +import BoundingBoxTool from '../selection-tools/bounding-box-tool'; +import NudgeTool from '../selection-tools/nudge-tool'; + +/** + * Tool for drawing ovals. + */ +class OvalTool extends paper.Tool { + static get TOLERANCE () { + return 6; + } + /** + * @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 + */ + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { + super(); + this.setSelectedItems = setSelectedItems; + this.clearSelectedItems = clearSelectedItems; + this.onUpdateSvg = onUpdateSvg; + this.boundingBoxTool = new BoundingBoxTool(Modes.OVAL, setSelectedItems, clearSelectedItems, onUpdateSvg); + const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg); + + // 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.onKeyUp = nudgeTool.onKeyUp; + this.onKeyDown = nudgeTool.onKeyDown; + + this.oval = null; + this.colorState = null; + this.isBoundingBoxMode = null; + this.active = false; + } + getHitOptions () { + 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: OvalTool.TOLERANCE / paper.view.zoom + }; + } + /** + * Should be called if the selection changes to update the bounds of the bounding box. + * @param {Array} selectedItems Array of selected items. + */ + onSelectionChanged (selectedItems) { + this.boundingBoxTool.onSelectionChanged(selectedItems); + } + setColorState (colorState) { + this.colorState = colorState; + } + handleMouseDown (event) { + if (event.event.button > 0) return; // only first mouse button + this.active = true; + + if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) { + this.isBoundingBoxMode = true; + } else { + this.isBoundingBoxMode = false; + clearSelection(this.clearSelectedItems); + this.oval = new paper.Shape.Ellipse({ + point: event.downPoint, + size: 0 + }); + styleShape(this.oval, this.colorState); + } + } + handleMouseDrag (event) { + if (event.event.button > 0 || !this.active) return; // only first mouse button + + if (this.isBoundingBoxMode) { + this.boundingBoxTool.onMouseDrag(event); + return; + } + + const downPoint = new paper.Point(event.downPoint.x, event.downPoint.y); + const point = new paper.Point(event.point.x, event.point.y); + if (event.modifiers.shift) { + this.oval.size = new paper.Point(event.downPoint.x - event.point.x, event.downPoint.x - event.point.x); + } else { + this.oval.size = downPoint.subtract(point); + } + if (event.modifiers.alt) { + this.oval.position = downPoint; + } else { + this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5)); + } + + } + handleMouseUp (event) { + if (event.event.button > 0 || !this.active) return; // only first mouse button + + if (this.isBoundingBoxMode) { + this.boundingBoxTool.onMouseUp(event); + this.isBoundingBoxMode = null; + return; + } + + if (this.oval) { + if (Math.abs(this.oval.size.width * this.oval.size.height) < OvalTool.TOLERANCE / paper.view.zoom) { + // Tiny oval created unintentionally? + this.oval.remove(); + this.oval = null; + } else { + const ovalPath = this.oval.toPath(true /* insert */); + this.oval.remove(); + this.oval = null; + + ovalPath.selected = true; + this.setSelectedItems(); + this.onUpdateSvg(); + } + } + this.active = false; + } + deactivateTool () { + this.boundingBoxTool.removeBoundsPath(); + } +} + +export default OvalTool; diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js new file mode 100644 index 00000000..8099f126 --- /dev/null +++ b/src/helper/bit-tools/rect-tool.js @@ -0,0 +1,127 @@ +import paper from '@scratch/paper'; +import Modes from '../../lib/modes'; +import {styleShape} from '../style-path'; +import {clearSelection} from '../selection'; +import BoundingBoxTool from '../selection-tools/bounding-box-tool'; +import NudgeTool from '../selection-tools/nudge-tool'; + +/** + * Tool for drawing rectangles. + */ +class RectTool extends paper.Tool { + static get TOLERANCE () { + return 6; + } + /** + * @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 + */ + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { + super(); + this.setSelectedItems = setSelectedItems; + this.clearSelectedItems = clearSelectedItems; + this.onUpdateSvg = onUpdateSvg; + this.boundingBoxTool = new BoundingBoxTool(Modes.RECT, setSelectedItems, clearSelectedItems, onUpdateSvg); + const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg); + + // 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.onKeyUp = nudgeTool.onKeyUp; + this.onKeyDown = nudgeTool.onKeyDown; + + this.rect = null; + this.colorState = null; + this.isBoundingBoxMode = null; + this.active = false; + } + getHitOptions () { + 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: RectTool.TOLERANCE / paper.view.zoom + }; + } + /** + * Should be called if the selection changes to update the bounds of the bounding box. + * @param {Array} selectedItems Array of selected items. + */ + onSelectionChanged (selectedItems) { + this.boundingBoxTool.onSelectionChanged(selectedItems); + } + setColorState (colorState) { + this.colorState = colorState; + } + handleMouseDown (event) { + if (event.event.button > 0) return; // only first mouse button + this.active = true; + + if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) { + this.isBoundingBoxMode = true; + } else { + this.isBoundingBoxMode = false; + clearSelection(this.clearSelectedItems); + } + } + handleMouseDrag (event) { + if (event.event.button > 0 || !this.active) return; // only first mouse button + + if (this.isBoundingBoxMode) { + this.boundingBoxTool.onMouseDrag(event); + return; + } + + if (this.rect) { + this.rect.remove(); + } + + const rect = new paper.Rectangle(event.downPoint, event.point); + if (event.modifiers.shift) { + rect.height = rect.width; + } + this.rect = new paper.Path.Rectangle(rect); + + if (event.modifiers.alt) { + this.rect.position = event.downPoint; + } + + styleShape(this.rect, this.colorState); + } + handleMouseUp (event) { + if (event.event.button > 0 || !this.active) return; // only first mouse button + + if (this.isBoundingBoxMode) { + this.boundingBoxTool.onMouseUp(event); + this.isBoundingBoxMode = null; + return; + } + + if (this.rect) { + if (this.rect.area < RectTool.TOLERANCE / paper.view.zoom) { + // Tiny rectangle created unintentionally? + this.rect.remove(); + this.rect = null; + } else { + this.rect.selected = true; + this.setSelectedItems(); + this.onUpdateSvg(); + this.rect = null; + } + } + this.active = false; + } + deactivateTool () { + this.boundingBoxTool.removeBoundsPath(); + } +} + +export default RectTool; diff --git a/src/helper/bit-tools/text-tool.js b/src/helper/bit-tools/text-tool.js new file mode 100644 index 00000000..24b3d064 --- /dev/null +++ b/src/helper/bit-tools/text-tool.js @@ -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} 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; diff --git a/src/helper/blob-tools/broad-brush-helper.js b/src/helper/blob-tools/broad-brush-helper.js index 17666511..dbd81eb6 100644 --- a/src/helper/blob-tools/broad-brush-helper.js +++ b/src/helper/blob-tools/broad-brush-helper.js @@ -2,7 +2,6 @@ import paper from '@scratch/paper'; import {styleBlob} from '../../helper/style-path'; import log from '../../log/log'; -import {getRaster} from '../../helper/layer'; /** * Broad brush functions to add as listeners on the mouse. Call them when the corresponding mouse event happens @@ -45,7 +44,6 @@ class BroadBrushHelper { }); styleBlob(this.finalPath, options); this.lastPoint = event.point; - getRaster().setPixel(event.point, 'blue'); } onBroadMouseDrag (event, tool, options) { diff --git a/src/lib/modes.js b/src/lib/modes.js index 7fcbbe57..4eafc6b5 100644 --- a/src/lib/modes.js +++ b/src/lib/modes.js @@ -1,6 +1,7 @@ import keyMirror from 'keymirror'; const Modes = keyMirror({ + BIT_BRUSH: null, BRUSH: null, ERASER: null, LINE: null, From ba51fe59a00e9bffdf714c12ced12aacca839df5 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 4 Apr 2018 23:54:41 -0400 Subject: [PATCH 03/17] Draw lines. Fix raster layer missing when changing costumes. --- src/containers/bit-brush-mode.jsx | 20 +++++++------- src/helper/bit-tools/brush-tool.js | 43 ++++++++++++++++++++++++++---- src/helper/layer.js | 9 +++++++ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index 183a72d0..9742458e 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -28,8 +28,8 @@ class BitBrushMode extends React.Component { } } componentWillReceiveProps (nextProps) { - if (this.tool && nextProps.colorState !== this.props.colorState) { - this.tool.setColorState(nextProps.colorState); + if (this.tool && nextProps.color !== this.props.color) { + this.tool.setColor(nextProps.color); } if (nextProps.isBitBrushModeActive && !this.props.isBitBrushModeActive) { @@ -44,14 +44,16 @@ class BitBrushMode extends React.Component { activateTool () { clearSelection(this.props.clearSelectedItems); // Force the default brush color if fill is MIXED or transparent - const {fillColor} = this.props.colorState; - if (fillColor === MIXED || fillColor === null) { + let color = this.props.color; + if (color === MIXED || color === null) { this.props.onChangeFillColor(DEFAULT_COLOR); + color = DEFAULT_COLOR; } this.tool = new BitBrushTool( this.props.onUpdateSvg ); - this.tool.setColorState(this.props.colorState); + this.tool.setColor(color); + this.tool.activate(); } deactivateTool () { @@ -74,11 +76,7 @@ BitBrushMode.propTypes = { brushSize: PropTypes.number.isRequired }), clearSelectedItems: PropTypes.func.isRequired, - colorState: PropTypes.shape({ - fillColor: PropTypes.string, - strokeColor: PropTypes.string, - strokeWidth: PropTypes.number - }).isRequired, + color: PropTypes.string, handleMouseDown: PropTypes.func.isRequired, isBitBrushModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, @@ -87,7 +85,7 @@ BitBrushMode.propTypes = { const mapStateToProps = state => ({ brushModeState: state.scratchPaint.brushMode, - colorState: state.scratchPaint.color, + color: state.scratchPaint.color.fillColor, isBitBrushModeActive: state.scratchPaint.mode === Modes.BIT_BRUSH }); const mapDispatchToProps = dispatch => ({ diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 6ec3ca39..11b4bc7a 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -20,14 +20,45 @@ class BrushTool extends paper.Tool { this.colorState = null; this.active = false; + this.lastPoint = null; } - setColorState (colorState) { - this.colorState = colorState; + setColor (color) { + this.color = color; + } + bresenhamLine (point1, point2, color, callback){ + // Fast Math.floor + let x1 = ~~point1.x; + let x2 = ~~point2.x; + let y1 = ~~point1.y; + let 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, color); + while (x1 !== x2 || y1 !== y2) { + let e2 = err*2; + if (e2 >-dy) { + err -= dy; x1 += sx; + } + if (e2 < dx) { + err += dx; y1 += sy; + } + callback(x1, y1, color); + } + } + // Draw a brush mark at the given point + draw (x, y, color) { + getRaster().setPixel(new paper.Point(x, y), color); } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button this.active = true; - getRaster().setPixel(event.point, 'blue'); + this.draw(event.point, event.point, this.color); + this.lastPoint = event.point; } handleMouseDrag (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button @@ -36,11 +67,13 @@ class BrushTool extends paper.Tool { this.boundingBoxTool.onMouseDrag(event); return; } - getRaster().setPixel(event.point, 'blue'); + this.bresenhamLine(this.lastPoint, event.point, this.color, this.draw); + this.lastPoint = event.point; } handleMouseUp (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button - getRaster().setPixel(event.point, 'blue'); + this.bresenhamLine(this.lastPoint, event.point, this.color, this.draw); + this.lastPoint = null; this.active = false; } deactivateTool () { diff --git a/src/helper/layer.js b/src/helper/layer.js index 15a05178..77d66d71 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -28,6 +28,15 @@ const clearRaster = function () { }; const getRaster = function () { + const layer = _getLayer('isRasterLayer'); + // Generate blank raster + if (layer.children.length === 0) { + const raster = new paper.Raster(rasterSrc); + raster.parent = layer; + raster.guide = true; + raster.locked = true; + raster.position = paper.view.center; + } return _getLayer('isRasterLayer').children[0]; }; From 80b4557741174776beed261d40e7e1b764c8fde8 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 5 Apr 2018 01:10:36 -0400 Subject: [PATCH 04/17] Add size, remove alias from other canvases --- src/components/loupe/loupe.jsx | 3 +++ src/helper/bit-tools/brush-tool.js | 21 +++++++++++++-------- src/helper/tools/eye-dropper.js | 4 ++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/loupe/loupe.jsx b/src/components/loupe/loupe.jsx index 127db61e..9e5bb499 100644 --- a/src/components/loupe/loupe.jsx +++ b/src/components/loupe/loupe.jsx @@ -36,6 +36,9 @@ class LoupeComponent extends React.Component { tmpCanvas.width = LOUPE_RADIUS * 2; tmpCanvas.height = LOUPE_RADIUS * 2; const tmpCtx = tmpCanvas.getContext('2d'); + tmpCtx.webkitImageSmoothingEnabled = false; + tmpCtx.mozImageSmoothingEnabled = false; + tmpCtx.imageSmoothingEnabled = false; const imageData = tmpCtx.createImageData( LOUPE_RADIUS * 2, LOUPE_RADIUS * 2 ); diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 11b4bc7a..d496a995 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -21,11 +21,12 @@ class BrushTool extends paper.Tool { this.colorState = null; this.active = false; this.lastPoint = null; + this.size = 5; } setColor (color) { this.color = color; } - bresenhamLine (point1, point2, color, callback){ + bresenhamLine (point1, point2, callback){ // Fast Math.floor let x1 = ~~point1.x; let x2 = ~~point2.x; @@ -38,7 +39,7 @@ class BrushTool extends paper.Tool { const sy = (y1 < y2) ? 1 : -1; let err = dx - dy; - callback(x1, y1, color); + callback(x1, y1); while (x1 !== x2 || y1 !== y2) { let e2 = err*2; if (e2 >-dy) { @@ -47,17 +48,21 @@ class BrushTool extends paper.Tool { if (e2 < dx) { err += dx; y1 += sy; } - callback(x1, y1, color); + callback(x1, y1); } } // Draw a brush mark at the given point - draw (x, y, color) { - getRaster().setPixel(new paper.Point(x, y), color); + draw (centerX, centerY) { + for (let x = centerX - this.size; x <= centerX + this.size; x++) { + for (let y = centerY - this.size; y <= centerY + this.size; y++) { + getRaster().setPixel(new paper.Point(x, y), this.color); + } + } } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button this.active = true; - this.draw(event.point, event.point, this.color); + this.draw(event.point, event.point); this.lastPoint = event.point; } handleMouseDrag (event) { @@ -67,12 +72,12 @@ class BrushTool extends paper.Tool { this.boundingBoxTool.onMouseDrag(event); return; } - this.bresenhamLine(this.lastPoint, event.point, this.color, this.draw); + this.bresenhamLine(this.lastPoint, event.point, this.draw.bind(this)); this.lastPoint = event.point; } handleMouseUp (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button - this.bresenhamLine(this.lastPoint, event.point, this.color, this.draw); + this.bresenhamLine(this.lastPoint, event.point, this.draw.bind(this)); this.lastPoint = null; this.active = false; } diff --git a/src/helper/tools/eye-dropper.js b/src/helper/tools/eye-dropper.js index d1fe2c3b..3eddc478 100644 --- a/src/helper/tools/eye-dropper.js +++ b/src/helper/tools/eye-dropper.js @@ -32,6 +32,10 @@ class EyeDropperTool extends paper.Tool { this.bufferCanvas = document.createElement('canvas'); this.bufferCanvas.width = canvas.width; this.bufferCanvas.height = canvas.height; + const context = this.bufferCanvas.getContext('2d') + context.webkitImageSmoothingEnabled = false; + context.mozImageSmoothingEnabled = false; + context.imageSmoothingEnabled = false; this.bufferImage = new Image(); this.bufferImage.onload = () => { this.bufferCanvas.getContext('2d').drawImage(this.bufferImage, 0, 0); From 887f528b0a16710a831dd3cd757c632a2e7a74d8 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 5 Apr 2018 01:32:27 -0400 Subject: [PATCH 05/17] allow transparent paintbrush --- src/containers/bit-brush-mode.jsx | 2 +- src/helper/bit-tools/brush-tool.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index 9742458e..c93c2ddd 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -45,7 +45,7 @@ class BitBrushMode extends React.Component { clearSelection(this.props.clearSelectedItems); // Force the default brush color if fill is MIXED or transparent let color = this.props.color; - if (color === MIXED || color === null) { + if (color === MIXED) { this.props.onChangeFillColor(DEFAULT_COLOR); color = DEFAULT_COLOR; } diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index d496a995..9ee77bb2 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -24,7 +24,7 @@ class BrushTool extends paper.Tool { this.size = 5; } setColor (color) { - this.color = color; + this.color = color ? color : new paper.Color(0, 0, 0, 0); } bresenhamLine (point1, point2, callback){ // Fast Math.floor From c6a282c97be3b0e79064b4116dce04a7ab5163f1 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Apr 2018 17:20:42 -0400 Subject: [PATCH 06/17] Add ellipse drawing algorithm and temp brush canvas for efficiency --- src/containers/bit-brush-mode.jsx | 2 +- src/helper/bit-tools/brush-tool.js | 95 +++++++++++++++++++++++++----- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index c93c2ddd..64d0ffa1 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -45,7 +45,7 @@ class BitBrushMode extends React.Component { clearSelection(this.props.clearSelectedItems); // Force the default brush color if fill is MIXED or transparent let color = this.props.color; - if (color === MIXED) { + if (!color || color === MIXED) { this.props.onChangeFillColor(DEFAULT_COLOR); color = DEFAULT_COLOR; } diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 9ee77bb2..3edfc1fa 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -21,17 +21,19 @@ class BrushTool extends paper.Tool { this.colorState = null; this.active = false; this.lastPoint = null; - this.size = 5; + // For performance, make sure this is an integer + this.size = 10; } setColor (color) { - this.color = color ? color : new paper.Color(0, 0, 0, 0); + this.color = color; } - bresenhamLine (point1, point2, callback){ + line (point1, point2, callback){ + // Bresenham line algorithm // Fast Math.floor let x1 = ~~point1.x; - let x2 = ~~point2.x; + const x2 = ~~point2.x; let y1 = ~~point1.y; - let y2 = ~~point2.y; + const y2 = ~~point2.y; const dx = Math.abs(x2 - x1); const dy = Math.abs(y2 - y1); @@ -41,8 +43,8 @@ class BrushTool extends paper.Tool { callback(x1, y1); while (x1 !== x2 || y1 !== y2) { - let e2 = err*2; - if (e2 >-dy) { + const e2 = err * 2; + if (e2 > -dy) { err -= dy; x1 += sx; } if (e2 < dx) { @@ -51,17 +53,79 @@ class BrushTool extends paper.Tool { callback(x1, y1); } } - // Draw a brush mark at the given point - draw (centerX, centerY) { - for (let x = centerX - this.size; x <= centerX + this.size; x++) { - for (let y = centerY - this.size; y <= centerY + this.size; y++) { - getRaster().setPixel(new paper.Point(x, y), this.color); + fillEllipse (centerX, centerY, radiusX, radiusY, context) { + // Bresenham ellipse algorithm + centerX = ~~centerX; + centerY = ~~centerY; + radiusX = ~~radiusX; + radiusY = ~~radiusY; + const twoRadXSquared = 2 * radiusX * radiusX; + const twoRadYSquared = 2 * radiusY * radiusY; + let x = radiusX; + let y = 0; + let dx = radiusY * radiusY * (1 - (radiusX << 1)); + let dy = radiusX * radiusX; + let error = 0; + let stoppingX = twoRadYSquared * radiusX; + let stoppingY = 0; + + while (stoppingX >= stoppingY) { + context.fillRect(centerX - x, centerY - y, x << 1, y << 1); + y++; + stoppingY += twoRadXSquared; + error += dy; + dy += twoRadXSquared; + if ((error << 1) + dx > 0) { + x--; + stoppingX -= twoRadYSquared; + error += dx; + dx += twoRadYSquared; } } + + x = 0; + y = radiusY; + dx = radiusY * radiusY; + dy = radiusX * radiusX * (1 - (radiusY << 1)); + error = 0; + stoppingX = 0; + stoppingY = twoRadXSquared * radiusY; + while (stoppingX <= stoppingY) { + context.fillRect(centerX - x, centerY - y, x * 2, y * 2); + x++; + stoppingX += twoRadYSquared; + error += dx; + dx += twoRadYSquared; + if ((error << 1) + dy > 0) { + y--; + stoppingY -= twoRadXSquared; + error += dy; + dy += twoRadXSquared; + } + + } + } + // Draw a brush mark at the given point + draw (x, y) { + getRaster().drawImage(this.tmpCanvas, new paper.Point(x - ~~(this.size / 2), y - ~~(this.size / 2))); } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button this.active = true; + + this.tmpCanvas = document.createElement('canvas'); + this.tmpCanvas.width = this.size; + this.tmpCanvas.height = this.size; + const context = this.tmpCanvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.fillStyle = this.color; + // Small squares for pixel artists + if (this.size <= 4) { + context.fillRect(0, 0, this.size, this.size); + } else { + this.fillEllipse(this.size / 2, this.size / 2, this.size / 2, this.size / 2, context); + } + this.draw(event.point, event.point); this.lastPoint = event.point; } @@ -72,12 +136,15 @@ class BrushTool extends paper.Tool { this.boundingBoxTool.onMouseDrag(event); return; } - this.bresenhamLine(this.lastPoint, event.point, this.draw.bind(this)); + this.line(this.lastPoint, event.point, this.draw.bind(this)); this.lastPoint = event.point; } handleMouseUp (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button - this.bresenhamLine(this.lastPoint, event.point, this.draw.bind(this)); + + this.line(this.lastPoint, event.point, this.draw.bind(this)); + + this.tmpCanvas = null; this.lastPoint = null; this.active = false; } From 40871b1c0fffa513e6538ebe03d8cdf8fbaba751 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 10 Apr 2018 18:02:30 -0400 Subject: [PATCH 07/17] Rename undo formats, and make format change on costume change skip convert --- src/containers/paper-canvas.jsx | 2 +- src/helper/undo.js | 8 ++++---- src/lib/format.js | 10 +++++----- src/reducers/undo.js | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 7941fa32..5655eb85 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -115,7 +115,7 @@ class PaperCanvas extends React.Component { this.props.clearHoveredItem(); this.props.clearPasteOffset(); if (svg) { - this.props.changeFormat(Formats.VECTOR); + this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT); // Store the zoom/pan and restore it after importing a new SVG const oldZoom = paper.project.view.zoom; const oldCenter = paper.project.view.center.clone(); diff --git a/src/helper/undo.js b/src/helper/undo.js index a069f516..3f9592d2 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -43,8 +43,8 @@ const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems, if (undoState.pointer > 0) { const state = undoState.stack[undoState.pointer - 1]; _restore(state, setSelectedItems, onUpdateSvg); - const format = isVector(state.paintEditorFormat) ? Formats.UNDO_VECTOR : - isBitmap(state.paintEditorFormat) ? Formats.UNDO_BITMAP : null; + const format = isVector(state.paintEditorFormat) ? Formats.VECTOR_SKIP_CONVERT : + isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null; if (!format) { log.error(`Invalid format: ${state.paintEditorFormat}`); } @@ -57,8 +57,8 @@ const performRedo = function (undoState, dispatchPerformRedo, setSelectedItems, if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) { const state = undoState.stack[undoState.pointer + 1]; _restore(state, setSelectedItems, onUpdateSvg); - const format = isVector(state.paintEditorFormat) ? Formats.UNDO_VECTOR : - isBitmap(state.paintEditorFormat) ? Formats.UNDO_BITMAP : null; + const format = isVector(state.paintEditorFormat) ? Formats.VECTOR_SKIP_CONVERT : + isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null; if (!format) { log.error(`Invalid format: ${state.paintEditorFormat}`); } diff --git a/src/lib/format.js b/src/lib/format.js index 0ae9a29a..abad144f 100644 --- a/src/lib/format.js +++ b/src/lib/format.js @@ -3,17 +3,17 @@ import keyMirror from 'keymirror'; const Formats = keyMirror({ BITMAP: null, VECTOR: null, - // Undo formats are conversions caused by the undo/redo stack - UNDO_BITMAP: null, - UNDO_VECTOR: null + // Format changes which should not trigger conversions, for instance undo + BITMAP_SKIP_CONVERT: null, + VECTOR_SKIP_CONVERT: null }); const isVector = function (format) { - return format === Formats.VECTOR || format === Formats.UNDO_VECTOR; + return format === Formats.VECTOR || format === Formats.VECTOR_SKIP_CONVERT; }; const isBitmap = function (format) { - return format === Formats.BITMAP || format === Formats.UNDO_BITMAP; + return format === Formats.BITMAP || format === Formats.BITMAP_SKIP_CONVERT; }; export { diff --git a/src/reducers/undo.js b/src/reducers/undo.js index 0ca26988..e372ffa2 100644 --- a/src/reducers/undo.js +++ b/src/reducers/undo.js @@ -64,7 +64,7 @@ const undoSnapshot = function (snapshot) { }; }; /** - * @param {Format} format Either UNDO_VECTOR or UNDO_BITMAP + * @param {Format} format Either VECTOR_SKIP_CONVERT or BITMAP_SKIP_CONVERT * @return {Action} undo action */ const undo = function (format) { @@ -74,7 +74,7 @@ const undo = function (format) { }; }; /** - * @param {Format} format Either UNDO_VECTOR or UNDO_BITMAP + * @param {Format} format Either VECTOR_SKIP_CONVERT or BITMAP_SKIP_CONVERT * @return {Action} undo action */ const redo = function (format) { From a868d29d82662a053dcb6d53b779eb974cfa29c2 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 11 Apr 2018 18:04:55 -0400 Subject: [PATCH 08/17] Add tool to bitmap editor --- src/components/paint-editor/paint-editor.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 38bca1aa..d84a4c40 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -36,7 +36,7 @@ import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicat import TextMode from '../../containers/text-mode.jsx'; import Formats from '../../lib/format'; -import {isVector} from '../../lib/format'; +import {isBitmap, isVector} from '../../lib/format'; import layout from '../../lib/layout-constants'; import styles from './paint-editor.css'; @@ -373,6 +373,11 @@ const PaintEditorComponent = props => { + + ) : null} + + {props.canvas !== null ? ( // eslint-disable-line no-negated-condition +
From 9c84537a420787717e3163c63290b1c7657eb8e0 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 12 Apr 2018 10:56:16 -0400 Subject: [PATCH 09/17] Switch tools when switching editors. Pipe through vector brush size for now. --- src/components/mode-tools/mode-tools.jsx | 1 + src/containers/bit-brush-mode.jsx | 6 +++++- src/containers/paint-editor.jsx | 13 ++++++++++++- src/helper/bit-tools/brush-tool.js | 6 ++++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index fd788ba3..7ad3f9c0 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -74,6 +74,7 @@ const ModeToolsComponent = props => { switch (props.mode) { case Modes.BRUSH: + case Modes.BIT_BRUSH: return (
diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index 64d0ffa1..5caed391 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -31,6 +31,9 @@ class BitBrushMode extends React.Component { if (this.tool && nextProps.color !== this.props.color) { this.tool.setColor(nextProps.color); } + if (this.tool && nextProps.brushModeState !== this.props.brushModeState) { + this.tool.setBrushSize(nextProps.brushModeState.brushSize); + } if (nextProps.isBitBrushModeActive && !this.props.isBitBrushModeActive) { this.activateTool(); @@ -53,6 +56,7 @@ class BitBrushMode extends React.Component { this.props.onUpdateSvg ); this.tool.setColor(color); + this.tool.setBrushSize(this.props.brushModeState.brushSize); this.tool.activate(); } @@ -72,7 +76,7 @@ class BitBrushMode extends React.Component { } BitBrushMode.propTypes = { - bitBrushModeState: PropTypes.shape({ + brushModeState: PropTypes.shape({ brushSize: PropTypes.number.isRequired }), clearSelectedItems: PropTypes.func.isRequired, diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 61feaf99..93a24104 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -23,7 +23,7 @@ import EyeDropperTool from '../helper/tools/eye-dropper'; import Modes from '../lib/modes'; import Formats from '../lib/format'; -import {isBitmap} from '../lib/format'; +import {isBitmap, isVector} from '../lib/format'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; @@ -79,6 +79,13 @@ class PaintEditor extends React.Component { } else if (!this.props.isEyeDropping && prevProps.isEyeDropping) { this.stopEyeDroppingLoop(); } + + // @todo move to correct corresponding tool + if (isVector(this.props.format) && isBitmap(prevProps.format)) { + this.props.changeMode(Modes.BRUSH); + } else if (isVector(prevProps.format) && isBitmap(this.props.format)) { + this.props.changeMode(Modes.BIT_BRUSH); + } } componentWillUnmount () { document.removeEventListener('keydown', this.props.onKeyPress); @@ -280,6 +287,7 @@ class PaintEditor extends React.Component { PaintEditor.propTypes = { changeColorToEyeDropper: PropTypes.func, + changeMode: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, format: PropTypes.oneOf(Object.keys(Formats)).isRequired, handleSwitchToBitmap: PropTypes.func.isRequired, @@ -344,6 +352,9 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeMode(Modes.RECT)); } }, + changeMode: mode => { + dispatch(changeMode(mode)); + }, clearSelectedItems: () => { dispatch(clearSelectedItems()); }, diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 3edfc1fa..13fcd937 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -21,12 +21,14 @@ class BrushTool extends paper.Tool { this.colorState = null; this.active = false; this.lastPoint = null; - // For performance, make sure this is an integer - this.size = 10; } setColor (color) { this.color = color; } + setBrushSize (size) { + // For performance, make sure this is an integer + this.size = ~~size; + } line (point1, point2, callback){ // Bresenham line algorithm // Fast Math.floor From c9c04745bc854d1059a983e8b67a31a93bcf1884 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 12 Apr 2018 14:23:19 -0400 Subject: [PATCH 10/17] Hide stroke indicators in bitmap mode --- src/components/paint-editor/paint-editor.jsx | 78 +++++++++++++------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index d84a4c40..5504d6aa 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -310,34 +310,56 @@ const PaintEditorComponent = props => {
{/* Second Row */} -
- - {/* fill */} - - {/* stroke */} - - {/* stroke width */} - - - - - -
+ {isVector(props.format) ? +
+ + {/* fill */} + + {/* stroke */} + + {/* stroke width */} + + + + + +
: +
+ + {/* fill */} + + + + + +
+ }
) : null} From 420e1013cf7aa2f44596a8f15d6fff96af7f6f52 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 12 Apr 2018 16:56:32 -0400 Subject: [PATCH 11/17] Use a separate state to track the size of the bitmap brush tool from the brush tool --- .../bit-brush-mode/bit-brush-mode.jsx | 2 +- src/components/bit-brush-mode/brush.svg | 10 +++ src/components/mode-tools/mode-tools.css | 6 ++ src/components/mode-tools/mode-tools.jsx | 23 ++++- src/containers/bit-brush-mode.jsx | 16 ++-- src/containers/brush-mode.jsx | 4 - src/helper/bit-tools/brush-tool.js | 86 ++----------------- src/helper/bitmap.js | 82 ++++++++++++++++++ src/reducers/bit-brush-size.js | 33 +++++++ src/reducers/scratch-paint-reducer.js | 2 + 10 files changed, 163 insertions(+), 101 deletions(-) create mode 100644 src/components/bit-brush-mode/brush.svg create mode 100644 src/reducers/bit-brush-size.js diff --git a/src/components/bit-brush-mode/bit-brush-mode.jsx b/src/components/bit-brush-mode/bit-brush-mode.jsx index 1535d983..d039e30d 100644 --- a/src/components/bit-brush-mode/bit-brush-mode.jsx +++ b/src/components/bit-brush-mode/bit-brush-mode.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx'; -import brushIcon from '../brush-mode/brush.svg'; // @todo: replace +import brushIcon from './brush.svg'; const BitBrushModeComponent = props => ( + + + brush + Created with Sketch. + + + + + \ No newline at end of file diff --git a/src/components/mode-tools/mode-tools.css b/src/components/mode-tools/mode-tools.css index e053bd9a..6d2b734f 100644 --- a/src/components/mode-tools/mode-tools.css +++ b/src/components/mode-tools/mode-tools.css @@ -11,6 +11,12 @@ margin-right: calc(2 * $grid-unit); width: 2rem; height: 2rem; + + image-rendering: -moz-crisp-edges; + image-rendering: -o-crisp-edges; + image-rendering: -webkit-optimize-contrast; + -ms-interpolation-mode: nearest-neighbor; + image-rendering: pixelated; } .mod-dashed-border { diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index 7ad3f9c0..f6ee28eb 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -6,6 +6,7 @@ import React from 'react'; import {changeBrushSize} from '../../reducers/brush-mode'; import {changeBrushSize as changeEraserSize} from '../../reducers/eraser-mode'; +import {changeBitBrushSize} from '../../reducers/bit-brush-size'; import LiveInputHOC from '../forms/live-input-hoc.jsx'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; @@ -13,12 +14,15 @@ import Input from '../forms/input.jsx'; import InputGroup from '../input-group/input-group.jsx'; import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx'; import Modes from '../../lib/modes'; +import Formats from '../../lib/format'; +import {isBitmap} from '../../lib/format'; import styles from './mode-tools.css'; import copyIcon from './icons/copy.svg'; import pasteIcon from './icons/paste.svg'; import brushIcon from '../brush-mode/brush.svg'; +import bitBrushIcon from '../bit-brush-mode/brush.svg'; import curvedPointIcon from './icons/curved-point.svg'; import eraserIcon from '../eraser-mode/eraser.svg'; import flipHorizontalIcon from './icons/flip-horizontal.svg'; @@ -75,6 +79,9 @@ const ModeToolsComponent = props => { switch (props.mode) { case Modes.BRUSH: case Modes.BIT_BRUSH: + const currentBrushIcon = isBitmap(props.format) ? bitBrushIcon : brushIcon; + const currentBrushValue = isBitmap(props.format) ? props.bitBrushSize : props.brushValue; + const changeFunction = isBitmap(props.format) ? props.onBitBrushSliderChange : props.onBrushSliderChange; return (
@@ -82,7 +89,7 @@ const ModeToolsComponent = props => { alt={props.intl.formatMessage(messages.brushSize)} className={styles.modeToolsIcon} draggable={false} - src={brushIcon} + src={currentBrushIcon} />
{ max={MAX_STROKE_WIDTH} min="1" type="number" - value={props.brushValue} - onSubmit={props.onBrushSliderChange} + value={currentBrushValue} + onSubmit={changeFunction} />
); @@ -175,15 +182,18 @@ const ModeToolsComponent = props => { }; ModeToolsComponent.propTypes = { + bitBrushSize: PropTypes.number, brushValue: PropTypes.number, className: PropTypes.string, clipboardItems: PropTypes.arrayOf(PropTypes.array), eraserValue: PropTypes.number, + format: PropTypes.oneOf(Object.keys(Formats)).isRequired, hasSelectedUncurvedPoints: PropTypes.bool, hasSelectedUnpointedPoints: PropTypes.bool, intl: intlShape.isRequired, mode: PropTypes.string.isRequired, - onBrushSliderChange: PropTypes.func, + onBitBrushSliderChange: PropTypes.func.isRequired, + onBrushSliderChange: PropTypes.func.isRequired, onCopyToClipboard: PropTypes.func.isRequired, onCurvePoints: PropTypes.func.isRequired, onEraserSliderChange: PropTypes.func, @@ -196,6 +206,8 @@ ModeToolsComponent.propTypes = { const mapStateToProps = state => ({ mode: state.scratchPaint.mode, + format: state.scratchPaint.format, + bitBrushSize: state.scratchPaint.bitBrushSize, brushValue: state.scratchPaint.brushMode.brushSize, clipboardItems: state.scratchPaint.clipboard.items, eraserValue: state.scratchPaint.eraserMode.brushSize, @@ -205,6 +217,9 @@ const mapDispatchToProps = dispatch => ({ onBrushSliderChange: brushSize => { dispatch(changeBrushSize(brushSize)); }, + onBitBrushSliderChange: bitBrushSize => { + dispatch(changeBitBrushSize(bitBrushSize)); + }, onEraserSliderChange: eraserSize => { dispatch(changeEraserSize(eraserSize)); } diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index 5caed391..633ede6e 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -6,7 +6,6 @@ import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; -import {changeBrushSize} from '../reducers/brush-mode'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelection} from '../helper/selection'; @@ -31,8 +30,8 @@ class BitBrushMode extends React.Component { if (this.tool && nextProps.color !== this.props.color) { this.tool.setColor(nextProps.color); } - if (this.tool && nextProps.brushModeState !== this.props.brushModeState) { - this.tool.setBrushSize(nextProps.brushModeState.brushSize); + if (this.tool && nextProps.bitBrushSize !== this.props.bitBrushSize) { + this.tool.setBrushSize(nextProps.bitBrushSize); } if (nextProps.isBitBrushModeActive && !this.props.isBitBrushModeActive) { @@ -56,7 +55,7 @@ class BitBrushMode extends React.Component { this.props.onUpdateSvg ); this.tool.setColor(color); - this.tool.setBrushSize(this.props.brushModeState.brushSize); + this.tool.setBrushSize(this.props.bitBrushSize); this.tool.activate(); } @@ -76,9 +75,7 @@ class BitBrushMode extends React.Component { } BitBrushMode.propTypes = { - brushModeState: PropTypes.shape({ - brushSize: PropTypes.number.isRequired - }), + bitBrushSize: PropTypes.number.isRequired, clearSelectedItems: PropTypes.func.isRequired, color: PropTypes.string, handleMouseDown: PropTypes.func.isRequired, @@ -88,7 +85,7 @@ BitBrushMode.propTypes = { }; const mapStateToProps = state => ({ - brushModeState: state.scratchPaint.brushMode, + bitBrushSize: state.scratchPaint.bitBrushSize, color: state.scratchPaint.color.fillColor, isBitBrushModeActive: state.scratchPaint.mode === Modes.BIT_BRUSH }); @@ -96,9 +93,6 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, - changeBrushSize: brushSize => { - dispatch(changeBrushSize(brushSize)); - }, handleMouseDown: () => { dispatch(changeMode(Modes.BIT_BRUSH)); }, diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index 6c089a8d..7584904a 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -7,7 +7,6 @@ import Blobbiness from '../helper/blob-tools/blob'; import {MIXED} from '../helper/style-path'; import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; -import {changeBrushSize} from '../reducers/brush-mode'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelection} from '../helper/selection'; @@ -98,9 +97,6 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, - changeBrushSize: brushSize => { - dispatch(changeBrushSize(brushSize)); - }, handleMouseDown: () => { dispatch(changeMode(Modes.BRUSH)); }, diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 13fcd937..524b5a18 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -1,5 +1,6 @@ import paper from '@scratch/paper'; import {getRaster} from '../layer'; +import {line, fillEllipse} from '../bitmap'; /** * Tool for drawing with the bitmap brush. @@ -29,84 +30,6 @@ class BrushTool extends paper.Tool { // For performance, make sure this is an integer this.size = ~~size; } - line (point1, point2, callback){ - // Bresenham line algorithm - // Fast Math.floor - let x1 = ~~point1.x; - 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; - if (e2 > -dy) { - err -= dy; x1 += sx; - } - if (e2 < dx) { - err += dx; y1 += sy; - } - callback(x1, y1); - } - } - fillEllipse (centerX, centerY, radiusX, radiusY, context) { - // Bresenham ellipse algorithm - centerX = ~~centerX; - centerY = ~~centerY; - radiusX = ~~radiusX; - radiusY = ~~radiusY; - const twoRadXSquared = 2 * radiusX * radiusX; - const twoRadYSquared = 2 * radiusY * radiusY; - let x = radiusX; - let y = 0; - let dx = radiusY * radiusY * (1 - (radiusX << 1)); - let dy = radiusX * radiusX; - let error = 0; - let stoppingX = twoRadYSquared * radiusX; - let stoppingY = 0; - - while (stoppingX >= stoppingY) { - context.fillRect(centerX - x, centerY - y, x << 1, y << 1); - y++; - stoppingY += twoRadXSquared; - error += dy; - dy += twoRadXSquared; - if ((error << 1) + dx > 0) { - x--; - stoppingX -= twoRadYSquared; - error += dx; - dx += twoRadYSquared; - } - } - - x = 0; - y = radiusY; - dx = radiusY * radiusY; - dy = radiusX * radiusX * (1 - (radiusY << 1)); - error = 0; - stoppingX = 0; - stoppingY = twoRadXSquared * radiusY; - while (stoppingX <= stoppingY) { - context.fillRect(centerX - x, centerY - y, x * 2, y * 2); - x++; - stoppingX += twoRadYSquared; - error += dx; - dx += twoRadYSquared; - if ((error << 1) + dy > 0) { - y--; - stoppingY -= twoRadXSquared; - error += dy; - dy += twoRadXSquared; - } - - } - } // Draw a brush mark at the given point draw (x, y) { getRaster().drawImage(this.tmpCanvas, new paper.Point(x - ~~(this.size / 2), y - ~~(this.size / 2))); @@ -125,7 +48,7 @@ class BrushTool extends paper.Tool { if (this.size <= 4) { context.fillRect(0, 0, this.size, this.size); } else { - this.fillEllipse(this.size / 2, this.size / 2, this.size / 2, this.size / 2, context); + fillEllipse(this.size / 2, this.size / 2, this.size / 2, this.size / 2, context); } this.draw(event.point, event.point); @@ -138,13 +61,14 @@ class BrushTool extends paper.Tool { this.boundingBoxTool.onMouseDrag(event); return; } - this.line(this.lastPoint, event.point, this.draw.bind(this)); + line(this.lastPoint, event.point, this.draw.bind(this)); this.lastPoint = event.point; } handleMouseUp (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button - this.line(this.lastPoint, event.point, this.draw.bind(this)); + line(this.lastPoint, event.point, this.draw.bind(this)); + this.onUpdateSvg(); this.tmpCanvas = null; this.lastPoint = null; diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 5c1db739..3b992b7c 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -1,5 +1,85 @@ import paper from '@scratch/paper'; +const line = function (point1, point2, callback) { + // Bresenham line algorithm + // Fast Math.floor + let x1 = ~~point1.x; + 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; + if (e2 > -dy) { + err -= dy; x1 += sx; + } + if (e2 < dx) { + err += dx; y1 += sy; + } + callback(x1, y1); + } +}; + +const fillEllipse = function (centerX, centerY, radiusX, radiusY, context) { + // Bresenham ellipse algorithm + centerX = ~~centerX; + centerY = ~~centerY; + radiusX = ~~radiusX; + radiusY = ~~radiusY; + const twoRadXSquared = 2 * radiusX * radiusX; + const twoRadYSquared = 2 * radiusY * radiusY; + let x = radiusX; + let y = 0; + let dx = radiusY * radiusY * (1 - (radiusX << 1)); + let dy = radiusX * radiusX; + let error = 0; + let stoppingX = twoRadYSquared * radiusX; + let stoppingY = 0; + + while (stoppingX >= stoppingY) { + context.fillRect(centerX - x, centerY - y, x << 1, y << 1); + y++; + stoppingY += twoRadXSquared; + error += dy; + dy += twoRadXSquared; + if ((error << 1) + dx > 0) { + x--; + stoppingX -= twoRadYSquared; + error += dx; + dx += twoRadYSquared; + } + } + + x = 0; + y = radiusY; + dx = radiusY * radiusY; + dy = radiusX * radiusX * (1 - (radiusY << 1)); + error = 0; + stoppingX = 0; + stoppingY = twoRadXSquared * radiusY; + while (stoppingX <= stoppingY) { + context.fillRect(centerX - x, centerY - y, x * 2, y * 2); + x++; + stoppingX += twoRadYSquared; + error += dx; + dx += twoRadYSquared; + if ((error << 1) + dy > 0) { + y--; + stoppingY -= twoRadXSquared; + error += dy; + dy += twoRadXSquared; + } + + } +}; + const rowBlank_ = function (imageData, width, y) { for (let x = 0; x < width; ++x) { if (imageData.data[(y * width << 2) + (x << 2) + 3] !== 0) return false; @@ -33,5 +113,7 @@ const trim = function (raster) { }; export { + fillEllipse, + line, trim }; diff --git a/src/reducers/bit-brush-size.js b/src/reducers/bit-brush-size.js new file mode 100644 index 00000000..3a0e6a9e --- /dev/null +++ b/src/reducers/bit-brush-size.js @@ -0,0 +1,33 @@ +import log from '../log/log'; + +// Bit brush size affects bit brush width, circle/rectangle outline drawing width, and line width +// in the bitmap paint editor. +const CHANGE_BIT_BRUSH_SIZE = 'scratch-paint/brush-mode/CHANGE_BIT_BRUSH_SIZE'; +const initialState = 5; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_BIT_BRUSH_SIZE: + if (isNaN(action.brushSize)) { + log.warn(`Invalid brush size: ${action.brushSize}`); + return state; + } + return Math.max(1, action.brushSize); + default: + return state; + } +}; + +// Action creators ================================== +const changeBitBrushSize = function (brushSize) { + return { + type: CHANGE_BIT_BRUSH_SIZE, + brushSize: brushSize + }; +}; + +export { + reducer as default, + changeBitBrushSize +}; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 224dcad7..3527051d 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -1,5 +1,6 @@ import {combineReducers} from 'redux'; import modeReducer from './modes'; +import bitBrushSizeReducer from './bit-brush-size'; import brushModeReducer from './brush-mode'; import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; @@ -14,6 +15,7 @@ import undoReducer from './undo'; export default combineReducers({ mode: modeReducer, + bitBrushSize: bitBrushSizeReducer, brushMode: brushModeReducer, color: colorReducer, clipboard: clipboardReducer, From ca42dc77352e4c84d9842b15c69ad18375eaf1f2 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 13 Apr 2018 11:33:47 -0400 Subject: [PATCH 12/17] Double pixel brush drawin working --- src/helper/bit-tools/brush-tool.js | 73 +++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 524b5a18..656cd0ea 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -1,6 +1,7 @@ import paper from '@scratch/paper'; import {getRaster} from '../layer'; import {line, fillEllipse} from '../bitmap'; +import {getGuideLayer} from '../layer'; /** * Tool for drawing with the bitmap brush. @@ -15,6 +16,7 @@ class BrushTool extends paper.Tool { // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. + this.onMouseMove = this.handleMouseMove; this.onMouseDown = this.handleMouseDown; this.onMouseDrag = this.handleMouseDrag; this.onMouseUp = this.handleMouseUp; @@ -22,36 +24,67 @@ class BrushTool extends paper.Tool { this.colorState = null; this.active = false; this.lastPoint = null; + this.cursorPreview = null; } setColor (color) { this.color = color; } setBrushSize (size) { + // TODO get 1px to work // For performance, make sure this is an integer - this.size = ~~size; + this.radius = Math.max(1, ~~(size / 2)); } // Draw a brush mark at the given point draw (x, y) { - getRaster().drawImage(this.tmpCanvas, new paper.Point(x - ~~(this.size / 2), y - ~~(this.size / 2))); + getRaster().drawImage(this.tmpCanvas, new paper.Point(~~x - this.radius, ~~y - this.radius)); + } + updateCursorIfNeeded () { + if (!this.radius) { + return; + } + // The cursor preview was unattached from the view by an outside process, + // such as changing costumes or undo. + if (this.cursorPreview && !this.cursorPreview.parent) { + this.cursorPreview = null; + } + + if (!this.cursorPreview || !(this.lastRadius === this.radius && this.lastColor === this.color)) { + if (this.cursorPreview) { + this.cursorPreview.remove(); + } + + this.tmpCanvas = document.createElement('canvas'); + this.tmpCanvas.width = this.radius * 2; + this.tmpCanvas.height = this.radius * 2; + const context = this.tmpCanvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.fillStyle = this.color; + // Small squares for pixel artists + if (this.radius <= 2) { + context.fillRect(0, 0, this.radius * 2, this.radius * 2); + } else { + fillEllipse(this.radius, this.radius, this.radius, this.radius, context); + } + + this.cursorPreview = new paper.Raster(this.tmpCanvas); + this.cursorPreview.guide = true; + this.cursorPreview.parent = getGuideLayer(); + this.cursorPreview.data.isHelperItem = true; + } + this.lastRadius = this.radius; + this.lastColor = this.color; + } + handleMouseMove (event) { + this.updateCursorIfNeeded(); + this.cursorPreview.position = new paper.Point(~~event.point.x, ~~event.point.y); } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button this.active = true; + + this.cursorPreview.remove(); - this.tmpCanvas = document.createElement('canvas'); - this.tmpCanvas.width = this.size; - this.tmpCanvas.height = this.size; - const context = this.tmpCanvas.getContext('2d'); - context.imageSmoothingEnabled = false; - context.fillStyle = this.color; - // Small squares for pixel artists - if (this.size <= 4) { - context.fillRect(0, 0, this.size, this.size); - } else { - fillEllipse(this.size / 2, this.size / 2, this.size / 2, this.size / 2, context); - } - - this.draw(event.point, event.point); + this.draw(event.point.x, event.point.y); this.lastPoint = event.point; } handleMouseDrag (event) { @@ -70,11 +103,17 @@ class BrushTool extends paper.Tool { line(this.lastPoint, event.point, this.draw.bind(this)); this.onUpdateSvg(); - this.tmpCanvas = null; this.lastPoint = null; this.active = false; + + this.updateCursorIfNeeded(); + this.cursorPreview.position = new paper.Point(~~event.point.x, ~~event.point.y); } deactivateTool () { + this.active = false; + this.tmpCanvas = null; + this.cursorPreview.remove(); + this.cursorPreview = null; } } From 87d93869060c7ec11af123c2aa1c80c76c6079f6 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 17 Apr 2018 14:49:04 -0400 Subject: [PATCH 13/17] Allow odd numbers for very small brush sizes for detail --- src/helper/bit-tools/brush-tool.js | 34 ++++++++++++++++++------------ src/reducers/bit-brush-size.js | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 656cd0ea..67b52a2f 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -30,16 +30,16 @@ class BrushTool extends paper.Tool { this.color = color; } setBrushSize (size) { - // TODO get 1px to work // For performance, make sure this is an integer - this.radius = Math.max(1, ~~(size / 2)); + this.size = Math.max(1, ~~size); } // Draw a brush mark at the given point draw (x, y) { - getRaster().drawImage(this.tmpCanvas, new paper.Point(~~x - this.radius, ~~y - this.radius)); + const roundedUpRadius = ~~(this.size / 2) + (this.size % 2); + getRaster().drawImage(this.tmpCanvas, new paper.Point(~~x - roundedUpRadius, ~~y - roundedUpRadius)); } updateCursorIfNeeded () { - if (!this.radius) { + if (!this.size) { return; } // The cursor preview was unattached from the view by an outside process, @@ -48,22 +48,28 @@ class BrushTool extends paper.Tool { this.cursorPreview = null; } - if (!this.cursorPreview || !(this.lastRadius === this.radius && this.lastColor === this.color)) { + if (!this.cursorPreview || !(this.lastSize === this.size && this.lastColor === this.color)) { if (this.cursorPreview) { this.cursorPreview.remove(); } this.tmpCanvas = document.createElement('canvas'); - this.tmpCanvas.width = this.radius * 2; - this.tmpCanvas.height = this.radius * 2; + const roundedUpRadius = ~~(this.size / 2) + (this.size % 2); + this.tmpCanvas.width = roundedUpRadius * 2; + this.tmpCanvas.height = roundedUpRadius * 2; const context = this.tmpCanvas.getContext('2d'); context.imageSmoothingEnabled = false; context.fillStyle = this.color; // Small squares for pixel artists - if (this.radius <= 2) { - context.fillRect(0, 0, this.radius * 2, this.radius * 2); + if (this.size <= 5) { + if (this.size % 2) { + context.fillRect(1, 1, this.size, this.size); + } else { + context.fillRect(0, 0, this.size, this.size); + } } else { - fillEllipse(this.radius, this.radius, this.radius, this.radius, context); + const roundedDownRadius = ~~(this.size / 2); + fillEllipse(roundedDownRadius, roundedDownRadius, roundedDownRadius, roundedDownRadius, context); } this.cursorPreview = new paper.Raster(this.tmpCanvas); @@ -71,7 +77,7 @@ class BrushTool extends paper.Tool { this.cursorPreview.parent = getGuideLayer(); this.cursorPreview.data.isHelperItem = true; } - this.lastRadius = this.radius; + this.lastSize = this.size; this.lastColor = this.color; } handleMouseMove (event) { @@ -112,8 +118,10 @@ class BrushTool extends paper.Tool { deactivateTool () { this.active = false; this.tmpCanvas = null; - this.cursorPreview.remove(); - this.cursorPreview = null; + if (this.cursorPreview) { + this.cursorPreview.remove(); + this.cursorPreview = null; + } } } diff --git a/src/reducers/bit-brush-size.js b/src/reducers/bit-brush-size.js index 3a0e6a9e..d4754fd7 100644 --- a/src/reducers/bit-brush-size.js +++ b/src/reducers/bit-brush-size.js @@ -3,7 +3,7 @@ import log from '../log/log'; // Bit brush size affects bit brush width, circle/rectangle outline drawing width, and line width // in the bitmap paint editor. const CHANGE_BIT_BRUSH_SIZE = 'scratch-paint/brush-mode/CHANGE_BIT_BRUSH_SIZE'; -const initialState = 5; +const initialState = 10; const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; From bda357bca6f810416d0dc5a2dbfa8b4f0563a4f3 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 17 Apr 2018 14:57:50 -0400 Subject: [PATCH 14/17] Delete extra tools --- src/helper/bit-tools/fill-tool.js | 177 ---------------- src/helper/bit-tools/oval-tool.js | 133 ------------ src/helper/bit-tools/rect-tool.js | 127 ------------ src/helper/bit-tools/text-tool.js | 326 ------------------------------ 4 files changed, 763 deletions(-) delete mode 100644 src/helper/bit-tools/fill-tool.js delete mode 100644 src/helper/bit-tools/oval-tool.js delete mode 100644 src/helper/bit-tools/rect-tool.js delete mode 100644 src/helper/bit-tools/text-tool.js diff --git a/src/helper/bit-tools/fill-tool.js b/src/helper/bit-tools/fill-tool.js deleted file mode 100644 index 9b43ff9f..00000000 --- a/src/helper/bit-tools/fill-tool.js +++ /dev/null @@ -1,177 +0,0 @@ -import paper from '@scratch/paper'; -import {getHoveredItem} from '../hover'; -import {expandBy} from '../math'; - -class FillTool extends paper.Tool { - static get TOLERANCE () { - return 2; - } - /** - * @param {function} setHoveredItem Callback to set the hovered item - * @param {function} clearHoveredItem Callback to clear the hovered item - * @param {!function} onUpdateSvg A callback to call when the image visibly changes - */ - constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) { - super(); - this.setHoveredItem = setHoveredItem; - this.clearHoveredItem = clearHoveredItem; - this.onUpdateSvg = onUpdateSvg; - - // We have to set these functions instead of just declaring them because - // paper.js tools hook up the listeners in the setter functions. - this.onMouseMove = this.handleMouseMove; - this.onMouseUp = this.handleMouseUp; - - // Color to fill with - this.fillColor = null; - // The path that's being hovered over. - this.fillItem = null; - // If we're hovering over a hole in a compound path, we can't just recolor it. This is the - // added item that's the same shape as the hole that's drawn over the hole when we fill a hole. - this.addedFillItem = null; - this.fillItemOrigColor = null; - this.prevHoveredItemId = null; - } - getHitOptions () { - const isAlmostClosedPath = function (item) { - return item instanceof paper.Path && item.segments.length > 2 && - item.lastSegment.point.getDistance(item.firstSegment.point) < 8; - }; - return { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false, - match: function (hitResult) { - 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 - }; - } - setFillColor (fillColor) { - this.fillColor = fillColor; - } - /** - * To be called when the hovered item changes. When the select tool hovers over a - * new item, it compares against this to see if a hover item change event needs to - * be fired. - * @param {paper.Item} prevHoveredItemId ID of the highlight item that indicates the mouse is - * over a given item currently - */ - setPrevHoveredItemId (prevHoveredItemId) { - this.prevHoveredItemId = prevHoveredItemId; - } - handleMouseMove (event) { - const hoveredItem = getHoveredItem(event, this.getHitOptions(), true /* subselect */); - if ((!hoveredItem && this.prevHoveredItemId) || // There is no longer a hovered item - (hoveredItem && !this.prevHoveredItemId) || // There is now a hovered item - (hoveredItem && this.prevHoveredItemId && - hoveredItem.id !== this.prevHoveredItemId)) { // hovered item changed - this.setHoveredItem(hoveredItem ? hoveredItem.id : null); - } - const hitItem = hoveredItem ? hoveredItem.data.origItem : null; - // Still hitting the same thing - if ((!hitItem && !this.fillItem) || this.fillItem === hitItem) { - return; - } - if (this.fillItem) { - if (this.addedFillItem) { - this.addedFillItem.remove(); - this.addedFillItem = null; - } else { - this._setFillItemColor(this.fillItemOrigColor); - } - this.fillItemOrigColor = null; - this.fillItem = null; - } - if (hitItem) { - this.fillItem = hitItem; - this.fillItemOrigColor = hitItem.fillColor; - if (hitItem.parent instanceof paper.CompoundPath && hitItem.area < 0) { // hole - if (!this.fillColor) { - // Hole filled with transparent is no-op - this.fillItem = null; - this.fillItemOrigColor = null; - return; - } - // Make an item to fill the hole - this.addedFillItem = hitItem.clone(); - this.addedFillItem.setClockwise(true); - this.addedFillItem.data.noHover = true; - this.addedFillItem.data.origItem = hitItem; - // This usually fixes it so there isn't a teeny tiny gap in between the fill and the outline - // when filling in a hole - expandBy(this.addedFillItem, .1); - this.addedFillItem.insertAbove(hitItem.parent); - } else if (this.fillItem.parent instanceof paper.CompoundPath) { - this.fillItemOrigColor = hitItem.parent.fillColor; - } - this._setFillItemColor(this.fillColor); - } - } - handleMouseUp (event) { - if (event.event.button > 0) return; // only first mouse button - if (this.fillItem) { - // If the hole we're filling in is the same color as the parent, and parent has no outline, remove the hole - if (this.addedFillItem && - this._noStroke(this.fillItem.parent) && - this.addedFillItem.fillColor.type !== 'gradient' && - this.fillItem.parent.fillColor.toCSS() === this.addedFillItem.fillColor.toCSS()) { - this.addedFillItem.remove(); - this.addedFillItem = null; - let parent = this.fillItem.parent; - this.fillItem.remove(); - parent = parent.reduce(); - parent.fillColor = this.fillColor; - } else if (this.addedFillItem) { - // Fill in a hole. - this.addedFillItem.data.noHover = false; - } else if (!this.fillColor && - this.fillItem.data && - this.fillItem.data.origItem) { - // Filling a hole filler with transparent returns it to being gone - // instead of making a shape that's transparent - const group = this.fillItem.parent; - this.fillItem.remove(); - if (!(group instanceof paper.Layer) && group.children.length === 1) { - group.reduce(); - } - } - - this.clearHoveredItem(); - this.fillItem = null; - this.addedFillItem = null; - this.fillItemOrigColor = null; - this.onUpdateSvg(); - } - } - _noStroke (item) { - return !item.strokeColor || - item.strokeColor.alpha === 0 || - item.strokeWidth === 0; - } - _setFillItemColor (color) { - if (this.addedFillItem) { - this.addedFillItem.fillColor = color; - } else if (this.fillItem.parent instanceof paper.CompoundPath) { - this.fillItem.parent.fillColor = color; - } else { - this.fillItem.fillColor = color; - } - } - deactivateTool () { - if (this.fillItem) { - this._setFillItemColor(this.fillItemOrigColor); - this.fillItemOrigColor = null; - this.fillItem = null; - } - this.clearHoveredItem(); - this.setHoveredItem = null; - this.clearHoveredItem = null; - } -} - -export default FillTool; diff --git a/src/helper/bit-tools/oval-tool.js b/src/helper/bit-tools/oval-tool.js deleted file mode 100644 index dc90006e..00000000 --- a/src/helper/bit-tools/oval-tool.js +++ /dev/null @@ -1,133 +0,0 @@ -import paper from '@scratch/paper'; -import Modes from '../../lib/modes'; -import {styleShape} from '../style-path'; -import {clearSelection} from '../selection'; -import BoundingBoxTool from '../selection-tools/bounding-box-tool'; -import NudgeTool from '../selection-tools/nudge-tool'; - -/** - * Tool for drawing ovals. - */ -class OvalTool extends paper.Tool { - static get TOLERANCE () { - return 6; - } - /** - * @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 - */ - constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { - super(); - this.setSelectedItems = setSelectedItems; - this.clearSelectedItems = clearSelectedItems; - this.onUpdateSvg = onUpdateSvg; - this.boundingBoxTool = new BoundingBoxTool(Modes.OVAL, setSelectedItems, clearSelectedItems, onUpdateSvg); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg); - - // 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.onKeyUp = nudgeTool.onKeyUp; - this.onKeyDown = nudgeTool.onKeyDown; - - this.oval = null; - this.colorState = null; - this.isBoundingBoxMode = null; - this.active = false; - } - getHitOptions () { - 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: OvalTool.TOLERANCE / paper.view.zoom - }; - } - /** - * Should be called if the selection changes to update the bounds of the bounding box. - * @param {Array} selectedItems Array of selected items. - */ - onSelectionChanged (selectedItems) { - this.boundingBoxTool.onSelectionChanged(selectedItems); - } - setColorState (colorState) { - this.colorState = colorState; - } - handleMouseDown (event) { - if (event.event.button > 0) return; // only first mouse button - this.active = true; - - if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) { - this.isBoundingBoxMode = true; - } else { - this.isBoundingBoxMode = false; - clearSelection(this.clearSelectedItems); - this.oval = new paper.Shape.Ellipse({ - point: event.downPoint, - size: 0 - }); - styleShape(this.oval, this.colorState); - } - } - handleMouseDrag (event) { - if (event.event.button > 0 || !this.active) return; // only first mouse button - - if (this.isBoundingBoxMode) { - this.boundingBoxTool.onMouseDrag(event); - return; - } - - const downPoint = new paper.Point(event.downPoint.x, event.downPoint.y); - const point = new paper.Point(event.point.x, event.point.y); - if (event.modifiers.shift) { - this.oval.size = new paper.Point(event.downPoint.x - event.point.x, event.downPoint.x - event.point.x); - } else { - this.oval.size = downPoint.subtract(point); - } - if (event.modifiers.alt) { - this.oval.position = downPoint; - } else { - this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5)); - } - - } - handleMouseUp (event) { - if (event.event.button > 0 || !this.active) return; // only first mouse button - - if (this.isBoundingBoxMode) { - this.boundingBoxTool.onMouseUp(event); - this.isBoundingBoxMode = null; - return; - } - - if (this.oval) { - if (Math.abs(this.oval.size.width * this.oval.size.height) < OvalTool.TOLERANCE / paper.view.zoom) { - // Tiny oval created unintentionally? - this.oval.remove(); - this.oval = null; - } else { - const ovalPath = this.oval.toPath(true /* insert */); - this.oval.remove(); - this.oval = null; - - ovalPath.selected = true; - this.setSelectedItems(); - this.onUpdateSvg(); - } - } - this.active = false; - } - deactivateTool () { - this.boundingBoxTool.removeBoundsPath(); - } -} - -export default OvalTool; diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js deleted file mode 100644 index 8099f126..00000000 --- a/src/helper/bit-tools/rect-tool.js +++ /dev/null @@ -1,127 +0,0 @@ -import paper from '@scratch/paper'; -import Modes from '../../lib/modes'; -import {styleShape} from '../style-path'; -import {clearSelection} from '../selection'; -import BoundingBoxTool from '../selection-tools/bounding-box-tool'; -import NudgeTool from '../selection-tools/nudge-tool'; - -/** - * Tool for drawing rectangles. - */ -class RectTool extends paper.Tool { - static get TOLERANCE () { - return 6; - } - /** - * @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 - */ - constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { - super(); - this.setSelectedItems = setSelectedItems; - this.clearSelectedItems = clearSelectedItems; - this.onUpdateSvg = onUpdateSvg; - this.boundingBoxTool = new BoundingBoxTool(Modes.RECT, setSelectedItems, clearSelectedItems, onUpdateSvg); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg); - - // 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.onKeyUp = nudgeTool.onKeyUp; - this.onKeyDown = nudgeTool.onKeyDown; - - this.rect = null; - this.colorState = null; - this.isBoundingBoxMode = null; - this.active = false; - } - getHitOptions () { - 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: RectTool.TOLERANCE / paper.view.zoom - }; - } - /** - * Should be called if the selection changes to update the bounds of the bounding box. - * @param {Array} selectedItems Array of selected items. - */ - onSelectionChanged (selectedItems) { - this.boundingBoxTool.onSelectionChanged(selectedItems); - } - setColorState (colorState) { - this.colorState = colorState; - } - handleMouseDown (event) { - if (event.event.button > 0) return; // only first mouse button - this.active = true; - - if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) { - this.isBoundingBoxMode = true; - } else { - this.isBoundingBoxMode = false; - clearSelection(this.clearSelectedItems); - } - } - handleMouseDrag (event) { - if (event.event.button > 0 || !this.active) return; // only first mouse button - - if (this.isBoundingBoxMode) { - this.boundingBoxTool.onMouseDrag(event); - return; - } - - if (this.rect) { - this.rect.remove(); - } - - const rect = new paper.Rectangle(event.downPoint, event.point); - if (event.modifiers.shift) { - rect.height = rect.width; - } - this.rect = new paper.Path.Rectangle(rect); - - if (event.modifiers.alt) { - this.rect.position = event.downPoint; - } - - styleShape(this.rect, this.colorState); - } - handleMouseUp (event) { - if (event.event.button > 0 || !this.active) return; // only first mouse button - - if (this.isBoundingBoxMode) { - this.boundingBoxTool.onMouseUp(event); - this.isBoundingBoxMode = null; - return; - } - - if (this.rect) { - if (this.rect.area < RectTool.TOLERANCE / paper.view.zoom) { - // Tiny rectangle created unintentionally? - this.rect.remove(); - this.rect = null; - } else { - this.rect.selected = true; - this.setSelectedItems(); - this.onUpdateSvg(); - this.rect = null; - } - } - this.active = false; - } - deactivateTool () { - this.boundingBoxTool.removeBoundsPath(); - } -} - -export default RectTool; diff --git a/src/helper/bit-tools/text-tool.js b/src/helper/bit-tools/text-tool.js deleted file mode 100644 index 24b3d064..00000000 --- a/src/helper/bit-tools/text-tool.js +++ /dev/null @@ -1,326 +0,0 @@ -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} 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; From d6111ed03120dec31065b25f59c2e84a52a7d24b Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 17 Apr 2018 15:01:10 -0400 Subject: [PATCH 15/17] Remove unused code --- src/components/loupe/loupe.jsx | 3 --- src/components/mode-tools/mode-tools.css | 6 ------ src/helper/tools/eye-dropper.js | 4 ---- 3 files changed, 13 deletions(-) diff --git a/src/components/loupe/loupe.jsx b/src/components/loupe/loupe.jsx index 9e5bb499..127db61e 100644 --- a/src/components/loupe/loupe.jsx +++ b/src/components/loupe/loupe.jsx @@ -36,9 +36,6 @@ class LoupeComponent extends React.Component { tmpCanvas.width = LOUPE_RADIUS * 2; tmpCanvas.height = LOUPE_RADIUS * 2; const tmpCtx = tmpCanvas.getContext('2d'); - tmpCtx.webkitImageSmoothingEnabled = false; - tmpCtx.mozImageSmoothingEnabled = false; - tmpCtx.imageSmoothingEnabled = false; const imageData = tmpCtx.createImageData( LOUPE_RADIUS * 2, LOUPE_RADIUS * 2 ); diff --git a/src/components/mode-tools/mode-tools.css b/src/components/mode-tools/mode-tools.css index 6d2b734f..e053bd9a 100644 --- a/src/components/mode-tools/mode-tools.css +++ b/src/components/mode-tools/mode-tools.css @@ -11,12 +11,6 @@ margin-right: calc(2 * $grid-unit); width: 2rem; height: 2rem; - - image-rendering: -moz-crisp-edges; - image-rendering: -o-crisp-edges; - image-rendering: -webkit-optimize-contrast; - -ms-interpolation-mode: nearest-neighbor; - image-rendering: pixelated; } .mod-dashed-border { diff --git a/src/helper/tools/eye-dropper.js b/src/helper/tools/eye-dropper.js index 3eddc478..d1fe2c3b 100644 --- a/src/helper/tools/eye-dropper.js +++ b/src/helper/tools/eye-dropper.js @@ -32,10 +32,6 @@ class EyeDropperTool extends paper.Tool { this.bufferCanvas = document.createElement('canvas'); this.bufferCanvas.width = canvas.width; this.bufferCanvas.height = canvas.height; - const context = this.bufferCanvas.getContext('2d') - context.webkitImageSmoothingEnabled = false; - context.mozImageSmoothingEnabled = false; - context.imageSmoothingEnabled = false; this.bufferImage = new Image(); this.bufferImage.onload = () => { this.bufferCanvas.getContext('2d').drawImage(this.bufferImage, 0, 0); From 15c3f44388b5f09d4dc9349145f3e0b3025778de Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 17 Apr 2018 15:55:40 -0400 Subject: [PATCH 16/17] Fix lint --- src/components/mode-tools/mode-tools.jsx | 3 +++ src/containers/bit-brush-mode.jsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index f6ee28eb..6356f9de 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -78,7 +78,9 @@ const ModeToolsComponent = props => { switch (props.mode) { case Modes.BRUSH: + /* falls through */ case Modes.BIT_BRUSH: + { const currentBrushIcon = isBitmap(props.format) ? bitBrushIcon : brushIcon; const currentBrushValue = isBitmap(props.format) ? props.bitBrushSize : props.brushValue; const changeFunction = isBitmap(props.format) ? props.onBitBrushSliderChange : props.onBrushSliderChange; @@ -103,6 +105,7 @@ const ModeToolsComponent = props => { />
); + } case Modes.ERASER: return (
diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index 633ede6e..eca2cf5f 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -41,7 +41,7 @@ class BitBrushMode extends React.Component { } } shouldComponentUpdate (nextProps) { - return nextProps.isBrushModeActive !== this.props.isBitBrushModeActive; + return nextProps.isBitBrushModeActive !== this.props.isBitBrushModeActive; } activateTool () { clearSelection(this.props.clearSelectedItems); From 88c9f179983fd2c6b17e0f9721f472a57b2ccf7a Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 20 Apr 2018 10:57:10 -0400 Subject: [PATCH 17/17] fix review comments --- src/helper/bit-tools/brush-tool.js | 10 +++++----- src/helper/bitmap.js | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 67b52a2f..da173899 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import {getRaster} from '../layer'; -import {line, fillEllipse} from '../bitmap'; +import {forEachLinePoint, fillEllipse} from '../bitmap'; import {getGuideLayer} from '../layer'; /** @@ -35,7 +35,7 @@ class BrushTool extends paper.Tool { } // Draw a brush mark at the given point draw (x, y) { - const roundedUpRadius = ~~(this.size / 2) + (this.size % 2); + const roundedUpRadius = Math.ceil(this.size / 2); getRaster().drawImage(this.tmpCanvas, new paper.Point(~~x - roundedUpRadius, ~~y - roundedUpRadius)); } updateCursorIfNeeded () { @@ -54,7 +54,7 @@ class BrushTool extends paper.Tool { } this.tmpCanvas = document.createElement('canvas'); - const roundedUpRadius = ~~(this.size / 2) + (this.size % 2); + const roundedUpRadius = Math.ceil(this.size / 2); this.tmpCanvas.width = roundedUpRadius * 2; this.tmpCanvas.height = roundedUpRadius * 2; const context = this.tmpCanvas.getContext('2d'); @@ -100,13 +100,13 @@ class BrushTool extends paper.Tool { this.boundingBoxTool.onMouseDrag(event); return; } - line(this.lastPoint, event.point, this.draw.bind(this)); + forEachLinePoint(this.lastPoint, event.point, this.draw.bind(this)); this.lastPoint = event.point; } handleMouseUp (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button - line(this.lastPoint, event.point, this.draw.bind(this)); + forEachLinePoint(this.lastPoint, event.point, this.draw.bind(this)); this.onUpdateSvg(); this.lastPoint = null; diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 3b992b7c..f4ab30a3 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -1,8 +1,7 @@ import paper from '@scratch/paper'; -const line = function (point1, point2, callback) { +const forEachLinePoint = function (point1, point2, callback) { // Bresenham line algorithm - // Fast Math.floor let x1 = ~~point1.x; const x2 = ~~point2.x; let y1 = ~~point1.y; @@ -18,10 +17,12 @@ const line = function (point1, point2, callback) { while (x1 !== x2 || y1 !== y2) { const e2 = err * 2; if (e2 > -dy) { - err -= dy; x1 += sx; + err -= dy; + x1 += sx; } if (e2 < dx) { - err += dx; y1 += sy; + err += dx; + y1 += sy; } callback(x1, y1); } @@ -114,6 +115,6 @@ const trim = function (raster) { export { fillEllipse, - line, + forEachLinePoint, trim };