diff --git a/src/containers/bit-fill-mode.jsx b/src/containers/bit-fill-mode.jsx index 8b6e7c2f..73753727 100644 --- a/src/containers/bit-fill-mode.jsx +++ b/src/containers/bit-fill-mode.jsx @@ -3,16 +3,18 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; +import GradientTypes from '../lib/gradient-types'; import FillModeComponent from '../components/bit-fill-mode/bit-fill-mode.jsx'; import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor2} from '../reducers/fill-color-2'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; -import {clearGradient} from '../reducers/selection-gradient-type'; +import {changeGradientType} from '../reducers/fill-mode-gradient-type'; import {clearSelection} from '../helper/selection'; import FillTool from '../helper/bit-tools/fill-tool'; -import {MIXED} from '../helper/style-path'; +import {getRotatedColor, MIXED} from '../helper/style-path'; class BitFillMode extends React.Component { constructor (props) { @@ -28,8 +30,16 @@ class BitFillMode extends React.Component { } } componentWillReceiveProps (nextProps) { - if (this.tool && nextProps.color !== this.props.color) { - this.tool.setColor(nextProps.color); + if (this.tool) { + if (nextProps.color !== this.props.color) { + this.tool.setColor(nextProps.color); + } + if (nextProps.color2 !== this.props.color2) { + this.tool.setColor2(nextProps.color2); + } + if (nextProps.fillModeGradientType !== this.props.fillModeGradientType) { + this.tool.setGradientType(nextProps.fillModeGradientType); + } } if (nextProps.isFillModeActive && !this.props.isFillModeActive) { @@ -43,14 +53,31 @@ class BitFillMode extends React.Component { } activateTool () { clearSelection(this.props.clearSelectedItems); - this.props.clearGradient(); + // Force the default brush color if fill is MIXED or transparent - const fillColorPresent = this.props.color !== MIXED && this.props.color !== null; - if (!fillColorPresent) { - this.props.onChangeFillColor(DEFAULT_COLOR); + let color = this.props.color; + if (this.props.color === MIXED) { + color = DEFAULT_COLOR; + this.props.onChangeFillColor(DEFAULT_COLOR, 0); + } + const gradientType = this.props.fillModeGradientType ? + this.props.fillModeGradientType : this.props.selectModeGradientType; + let color2 = this.props.color2; + if (gradientType !== this.props.selectModeGradientType) { + if (this.props.selectModeGradientType === GradientTypes.SOLID) { + color2 = getRotatedColor(color); + this.props.onChangeFillColor(color2, 1); + } + this.props.changeGradientType(gradientType); + } + if (this.props.color2 === MIXED) { + color2 = getRotatedColor(); + this.props.onChangeFillColor(color2, 1); } this.tool = new FillTool(this.props.onUpdateImage); - this.tool.setColor(this.props.color); + this.tool.setColor(color); + this.tool.setColor2(color2); + this.tool.setGradientType(gradientType); this.tool.activate(); } deactivateTool () { @@ -69,31 +96,41 @@ class BitFillMode extends React.Component { } BitFillMode.propTypes = { - clearGradient: PropTypes.func.isRequired, + changeGradientType: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, color: PropTypes.string, + color2: PropTypes.string, + fillModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)), handleMouseDown: PropTypes.func.isRequired, isFillModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, - onUpdateImage: PropTypes.func.isRequired + onUpdateImage: PropTypes.func.isRequired, + selectModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired }; const mapStateToProps = state => ({ + fillModeGradientType: state.scratchPaint.fillMode.gradientType, // Last user-selected gradient type color: state.scratchPaint.color.fillColor, - isFillModeActive: state.scratchPaint.mode === Modes.BIT_FILL + color2: state.scratchPaint.color.fillColor2, + isFillModeActive: state.scratchPaint.mode === Modes.BIT_FILL, + selectModeGradientType: state.scratchPaint.color.gradientType }); const mapDispatchToProps = dispatch => ({ - clearGradient: () => { - dispatch(clearGradient()); - }, clearSelectedItems: () => { dispatch(clearSelectedItems()); }, + changeGradientType: gradientType => { + dispatch(changeGradientType(gradientType)); + }, handleMouseDown: () => { dispatch(changeMode(Modes.BIT_FILL)); }, - onChangeFillColor: fillColor => { - dispatch(changeFillColor(fillColor)); + onChangeFillColor: (fillColor, index) => { + if (index === 0) { + dispatch(changeFillColor(fillColor)); + } else if (index === 1) { + dispatch(changeFillColor2(fillColor)); + } } }); diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index b6a26907..44a4ba9e 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -13,7 +13,7 @@ import {getSelectedLeafItems} from '../helper/selection'; import {setSelectedItems} from '../reducers/selected-items'; import Modes from '../lib/modes'; import Formats from '../lib/format'; -import {isBitmap, isVector} from '../lib/format'; +import {isBitmap} from '../lib/format'; import GradientTypes from '../lib/gradient-types'; import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; @@ -122,10 +122,10 @@ const mapStateToProps = state => ({ gradientType: state.scratchPaint.color.gradientType, isEyeDropping: state.scratchPaint.color.eyeDropper.active, mode: state.scratchPaint.mode, - shouldShowGradientTools: isVector(state.scratchPaint.format) && - (state.scratchPaint.mode === Modes.SELECT || + shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT || state.scratchPaint.mode === Modes.RESHAPE || - state.scratchPaint.mode === Modes.FILL), + state.scratchPaint.mode === Modes.FILL || + state.scratchPaint.mode === Modes.BIT_FILL, textEditTarget: state.scratchPaint.textEditTarget }); diff --git a/src/helper/bit-tools/fill-tool.js b/src/helper/bit-tools/fill-tool.js index 3f9ebd8a..4320e2ef 100644 --- a/src/helper/bit-tools/fill-tool.js +++ b/src/helper/bit-tools/fill-tool.js @@ -1,6 +1,8 @@ import paper from '@scratch/paper'; -import {floodFill, floodFillAll} from '../bitmap'; -import {getRaster} from '../layer'; +import {floodFill, floodFillAll, getHitBounds} from '../bitmap'; +import {createGradientObject} from '../style-path'; +import {createCanvas, getRaster} from '../layer'; +import GradientTypes from '../../lib/gradient-types'; const TRANSPARENT = 'rgba(0,0,0,0)'; /** @@ -13,48 +15,92 @@ class FillTool extends paper.Tool { constructor (onUpdateImage) { super(); this.onUpdateImage = onUpdateImage; - + // 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.color = null; - this.changed = false; + this.color2 = null; + this.gradientType = null; this.active = false; } setColor (color) { - // Null color means transparent because that is the standard in vector - this.color = color ? color : TRANSPARENT; + this.color = color; + } + setColor2 (color2) { + this.color2 = color2; + } + setGradientType (gradientType) { + this.gradientType = gradientType; } handleMouseDown (event) { - const context = getRaster().getContext('2d'); - if (event.event.shiftKey) { - this.changed = floodFillAll(event.point.x, event.point.y, this.color, context) || this.changed; - } else { - this.changed = floodFill(event.point.x, event.point.y, this.color, context) || this.changed; - } + this.paint(event); } handleMouseDrag (event) { - const context = getRaster().getContext('2d'); - if (event.event.shiftKey) { - this.changed = floodFillAll(event.point.x, event.point.y, this.color, context) || this.changed; - } else { - this.changed = floodFill(event.point.x, event.point.y, this.color, context) || this.changed; - } + this.paint(event); } - handleMouseUp () { - if (this.changed) { + paint (event) { + const sourceContext = getRaster().getContext('2d'); + let destContext = sourceContext; + let color = this.color; + // Paint to a mask instead of the original canvas when drawing + if (this.gradientType !== GradientTypes.SOLID) { + const tmpCanvas = createCanvas(); + destContext = tmpCanvas.getContext('2d'); + color = 'black'; + } else if (!color) { + // Null color means transparent because that is the standard in vector + color = TRANSPARENT; + } + let changed = false; + if (event.event.shiftKey) { + changed = floodFillAll(event.point.x, event.point.y, color, sourceContext, destContext); + } else { + changed = floodFill(event.point.x, event.point.y, color, sourceContext, destContext); + } + if (changed && this.gradientType !== GradientTypes.SOLID) { + const raster = new paper.Raster({insert: false}); + raster.canvas = destContext.canvas; + raster.onLoad = () => { + raster.position = getRaster().position; + // Erase what's already there + getRaster().getContext().globalCompositeOperation = 'destination-out'; + getRaster().drawImage(raster.canvas, new paper.Point()); + getRaster().getContext().globalCompositeOperation = 'source-over'; + + // Create the gradient to be masked + const hitBounds = getHitBounds(raster); + if (!hitBounds.area) return; + const gradient = new paper.Shape.Rectangle({ + insert: false, + rectangle: { + topLeft: hitBounds.topLeft, + bottomRight: hitBounds.bottomRight + } + }); + gradient.fillColor = createGradientObject( + this.color, + this.color2, + this.gradientType, + gradient.bounds, + event.point); + const rasterGradient = gradient.rasterize(getRaster().resolution.width, false /* insert */); + + // Mask gradient + raster.getContext().globalCompositeOperation = 'source-in'; + raster.drawImage(rasterGradient.canvas, rasterGradient.bounds.topLeft); + + // Draw masked gradient into raster layer + getRaster().drawImage(raster.canvas, new paper.Point()); + this.onUpdateImage(); + }; + } else if (changed) { this.onUpdateImage(); - this.changed = false; } } deactivateTool () { - if (this.changed) { - this.onUpdateImage(); - this.changed = false; - } } } diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 85df96d1..d1c79d99 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -429,23 +429,26 @@ const colorPixel_ = function (x, y, imageData, newColor) { * * @param {!int} x The x coordinate on the context at which to begin * @param {!int} y The y coordinate on the context at which to begin - * @param {!ImageData} imageData The image data to edit + * @param {!ImageData} sourceImageData The image data to sample from. This is edited by the function. + * @param {!ImageData} destImageData The image data to edit. May match sourceImageData. Should match + * size of sourceImageData. * @param {!Array} newColor The color to replace with. A length 4 array [r, g, b, a]. * @param {!Array} oldColor The color to replace. A length 4 array [r, g, b, a]. * This must be different from newColor. * @param {!Array>} stack The stack of pixels we need to look at */ -const floodFillInternal_ = function (x, y, imageData, newColor, oldColor, stack) { - while (y > 0 && matchesColor_(x, y - 1, imageData, oldColor)) { +const floodFillInternal_ = function (x, y, sourceImageData, destImageData, newColor, oldColor, stack) { + while (y > 0 && matchesColor_(x, y - 1, sourceImageData, oldColor)) { y--; } let lastLeftMatchedColor = false; let lastRightMatchedColor = false; - for (; y < imageData.height; y++) { - if (!matchesColor_(x, y, imageData, oldColor)) break; - colorPixel_(x, y, imageData, newColor); + for (; y < sourceImageData.height; y++) { + if (!matchesColor_(x, y, sourceImageData, oldColor)) break; + colorPixel_(x, y, sourceImageData, newColor); + colorPixel_(x, y, destImageData, newColor); if (x > 0) { - if (matchesColor_(x - 1, y, imageData, oldColor)) { + if (matchesColor_(x - 1, y, sourceImageData, oldColor)) { if (!lastLeftMatchedColor) { stack.push([x - 1, y]); lastLeftMatchedColor = true; @@ -454,8 +457,8 @@ const floodFillInternal_ = function (x, y, imageData, newColor, oldColor, stack) lastLeftMatchedColor = false; } } - if (x < imageData.width - 1) { - if (matchesColor_(x + 1, y, imageData, oldColor)) { + if (x < sourceImageData.width - 1) { + if (matchesColor_(x + 1, y, sourceImageData, oldColor)) { if (!lastRightMatchedColor) { stack.push([x + 1, y]); lastRightMatchedColor = true; @@ -487,15 +490,21 @@ const fillStyleToColor_ = function (fillStyleString) { * @param {!number} x The x coordinate on the context at which to begin * @param {!number} y The y coordinate on the context at which to begin * @param {!string} color A color string, which would go into context.fillStyle - * @param {!HTMLCanvas2DContext} context The context in which to draw + * @param {!HTMLCanvas2DContext} sourceContext The context from which to sample to determine where to flood fill + * @param {!HTMLCanvas2DContext} destContext The context to which to draw. May match sourceContext. Should match + * the size of sourceContext. * @return {boolean} True if image changed, false otherwise */ -const floodFill = function (x, y, color, context) { +const floodFill = function (x, y, color, sourceContext, destContext) { x = ~~x; y = ~~y; const newColor = fillStyleToColor_(color); - const oldColor = getColor_(x, y, context); - const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height); + const oldColor = getColor_(x, y, sourceContext); + const sourceImageData = sourceContext.getImageData(0, 0, sourceContext.canvas.width, sourceContext.canvas.height); + let destImageData = sourceImageData; + if (destContext !== sourceContext) { + destImageData = new ImageData(sourceContext.canvas.width, sourceContext.canvas.height); + } if (oldColor[0] === newColor[0] && oldColor[1] === newColor[1] && oldColor[2] === newColor[2] && @@ -505,9 +514,9 @@ const floodFill = function (x, y, color, context) { const stack = [[x, y]]; while (stack.length) { const pop = stack.pop(); - floodFillInternal_(pop[0], pop[1], imageData, newColor, oldColor, stack); + floodFillInternal_(pop[0], pop[1], sourceImageData, destImageData, newColor, oldColor, stack); } - context.putImageData(imageData, 0, 0); + destContext.putImageData(destImageData, 0, 0); return true; }; @@ -516,29 +525,34 @@ const floodFill = function (x, y, color, context) { * @param {!number} x The x coordinate on the context of the start color * @param {!number} y The y coordinate on the context of the start color * @param {!string} color A color string, which would go into context.fillStyle - * @param {!HTMLCanvas2DContext} context The context in which to draw + * @param {!HTMLCanvas2DContext} sourceContext The context from which to sample to determine where to flood fill + * @param {!HTMLCanvas2DContext} destContext The context to which to draw. May match sourceContext. Should match * @return {boolean} True if image changed, false otherwise */ -const floodFillAll = function (x, y, color, context) { +const floodFillAll = function (x, y, color, sourceContext, destContext) { x = ~~x; y = ~~y; const newColor = fillStyleToColor_(color); - const oldColor = getColor_(x, y, context); - const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height); + const oldColor = getColor_(x, y, sourceContext); + const sourceImageData = sourceContext.getImageData(0, 0, sourceContext.canvas.width, sourceContext.canvas.height); + let destImageData = sourceImageData; + if (destContext !== sourceContext) { + destImageData = new ImageData(sourceContext.canvas.width, sourceContext.canvas.height); + } if (oldColor[0] === newColor[0] && oldColor[1] === newColor[1] && oldColor[2] === newColor[2] && oldColor[3] === newColor[3]) { // no-op return false; } - for (let i = 0; i < imageData.width; i++) { - for (let j = 0; j < imageData.height; j++) { - if (matchesColor_(i, j, imageData, oldColor)) { - colorPixel_(i, j, imageData, newColor); + for (let i = 0; i < sourceImageData.width; i++) { + for (let j = 0; j < sourceImageData.height; j++) { + if (matchesColor_(i, j, sourceImageData, oldColor)) { + colorPixel_(i, j, destImageData, newColor); } } } - context.putImageData(imageData, 0, 0); + destContext.putImageData(destImageData, 0, 0); return true; }; diff --git a/src/helper/style-path.js b/src/helper/style-path.js index ddedbf32..040dc7b4 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -55,6 +55,42 @@ const getRotatedColor = function (firstColor) { `hsl(${(color.hue - 72) % 360}, ${color.saturation * 100}, ${Math.max(color.lightness * 100, 10)})`).hex; }; +/** + * Convert params to a paper.Color gradient object + * @param {?string} color1 CSS string, or null for transparent + * @param {?string} color2 CSS string, or null for transparent + * @param {GradientType} gradientType gradient type + * @param {paper.Rectangle} bounds Bounds of the object + * @param {paper.Point} radialCenter Where the center of a radial gradient should be, if the gradient is radial + * @return {paper.Color} Color object with gradient, may be null or color string if the gradient type is solid + */ +const createGradientObject = function (color1, color2, gradientType, bounds, radialCenter) { + if (gradientType === GradientTypes.SOLID) return color1; + if (color1 === null) { + color1 = getColorStringForTransparent(color2); + } + if (color2 === null) { + color2 = getColorStringForTransparent(color1); + } + const halfLongestDimension = Math.max(bounds.width, bounds.height) / 2; + const start = gradientType === GradientTypes.RADIAL ? radialCenter : + gradientType === GradientTypes.VERTICAL ? bounds.topCenter : + gradientType === GradientTypes.HORIZONTAL ? bounds.leftCenter : + null; + const end = gradientType === GradientTypes.RADIAL ? start.add(new paper.Point(halfLongestDimension, 0)) : + gradientType === GradientTypes.VERTICAL ? bounds.bottomCenter : + gradientType === GradientTypes.HORIZONTAL ? bounds.rightCenter : + null; + return { + gradient: { + stops: [color1, color2], + radial: gradientType === GradientTypes.RADIAL + }, + origin: start, + destination: end + }; +}; + /** * Called when setting fill color * @param {string} colorString color, css format, or null if completely transparent @@ -497,8 +533,8 @@ export { applyGradientTypeToSelection, applyStrokeColorToSelection, applyStrokeWidthToSelection, + createGradientObject, getColorsFromSelection, - getColorStringForTransparent, getRotatedColor, MIXED, styleBlob, diff --git a/src/helper/tools/fill-tool.js b/src/helper/tools/fill-tool.js index ac447666..611efd02 100644 --- a/src/helper/tools/fill-tool.js +++ b/src/helper/tools/fill-tool.js @@ -1,7 +1,7 @@ import paper from '@scratch/paper'; import {getHoveredItem} from '../hover'; import {expandBy} from '../math'; -import {getColorStringForTransparent} from '../style-path'; +import {createGradientObject} from '../style-path'; import GradientTypes from '../../lib/gradient-types'; class FillTool extends paper.Tool { @@ -176,37 +176,13 @@ class FillTool extends paper.Tool { // Either pass in a fully defined paper.Color as color1, // or pass in 2 color strings, a gradient type, and a pointer location _setFillItemColor (color1, color2, gradientType, pointerLocation) { - let fillColor; const item = this._getFillItem(); if (!item) return; if (color1 instanceof paper.Color || gradientType === GradientTypes.SOLID) { - fillColor = color1; + item.fillColor = color1; } else { - if (color1 === null) { - color1 = getColorStringForTransparent(color2); - } - if (color2 === null) { - color2 = getColorStringForTransparent(color1); - } - const halfLongestDimension = Math.max(item.bounds.width, item.bounds.height) / 2; - const start = gradientType === GradientTypes.RADIAL ? pointerLocation : - gradientType === GradientTypes.VERTICAL ? item.bounds.topCenter : - gradientType === GradientTypes.HORIZONTAL ? item.bounds.leftCenter : - null; - const end = gradientType === GradientTypes.RADIAL ? start.add(new paper.Point(halfLongestDimension, 0)) : - gradientType === GradientTypes.VERTICAL ? item.bounds.bottomCenter : - gradientType === GradientTypes.HORIZONTAL ? item.bounds.rightCenter : - null; - fillColor = { - gradient: { - stops: [color1, color2], - radial: gradientType === GradientTypes.RADIAL - }, - origin: start, - destination: end - }; + item.fillColor = createGradientObject(color1, color2, gradientType, item.bounds, pointerLocation); } - item.fillColor = fillColor; } _getFillItem () { if (this.addedFillItem) {