Bitmap gradient (#559)

This commit is contained in:
DD Liu 2018-07-17 17:21:02 -04:00 committed by GitHub
parent 4ba79cacbb
commit af3c6694d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 208 additions and 99 deletions

View file

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

View file

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

View file

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

View file

@ -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<number>} newColor The color to replace with. A length 4 array [r, g, b, a].
* @param {!Array<number>} oldColor The color to replace. A length 4 array [r, g, b, a].
* This must be different from newColor.
* @param {!Array<Array<int>>} 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;
};

View file

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

View file

@ -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) {