Make style-path helpers generic over fill & stroke

This commit is contained in:
adroitwhiz 2020-04-15 09:51:07 -04:00
parent dc430a0111
commit 1ecab99cfb
6 changed files with 215 additions and 120 deletions

View file

@ -16,7 +16,7 @@ import {isBitmap} from '../lib/format';
import GradientTypes from '../lib/gradient-types'; import GradientTypes from '../lib/gradient-types';
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
import {applyFillColorToSelection, import {applyColorToSelection,
applyGradientTypeToSelection, applyGradientTypeToSelection,
getRotatedColor, getRotatedColor,
swapColorsInSelection, swapColorsInSelection,
@ -45,11 +45,12 @@ class FillColorIndicator extends React.Component {
} }
handleChangeFillColor (newColor) { handleChangeFillColor (newColor) {
// Apply color and update redux, but do not update svg until picker closes. // Apply color and update redux, but do not update svg until picker closes.
const isDifferent = applyFillColorToSelection( const isDifferent = applyColorToSelection(
newColor, newColor,
this.props.colorIndex, this.props.colorIndex,
this.props.gradientType === GradientTypes.SOLID, this.props.gradientType === GradientTypes.SOLID,
isBitmap(this.props.format), isBitmap(this.props.format),
false, // applyToStroke
this.props.textEditTarget); this.props.textEditTarget);
this._hasChanged = this._hasChanged || isDifferent; this._hasChanged = this._hasChanged || isDifferent;
this.props.onChangeFillColor(newColor, this.props.colorIndex); this.props.onChangeFillColor(newColor, this.props.colorIndex);
@ -59,6 +60,7 @@ class FillColorIndicator extends React.Component {
const isDifferent = applyGradientTypeToSelection( const isDifferent = applyGradientTypeToSelection(
gradientType, gradientType,
isBitmap(this.props.format), isBitmap(this.props.format),
false, // applyToStroke
this.props.textEditTarget); this.props.textEditTarget);
this._hasChanged = this._hasChanged || isDifferent; this._hasChanged = this._hasChanged || isDifferent;
const hasSelectedItems = getSelectedLeafItems().length > 0; const hasSelectedItems = getSelectedLeafItems().length > 0;
@ -92,6 +94,7 @@ class FillColorIndicator extends React.Component {
if (getSelectedLeafItems().length) { if (getSelectedLeafItems().length) {
const isDifferent = swapColorsInSelection( const isDifferent = swapColorsInSelection(
isBitmap(this.props.format), isBitmap(this.props.format),
false, // applyToStroke
this.props.textEditTarget); this.props.textEditTarget);
this.props.setSelectedItems(); this.props.setSelectedItems();
this._hasChanged = this._hasChanged || isDifferent; this._hasChanged = this._hasChanged || isDifferent;

View file

@ -10,7 +10,7 @@ import Formats from '../lib/format';
import {isBitmap} from '../lib/format'; import {isBitmap} from '../lib/format';
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
import {applyStrokeColorToSelection, applyStrokeWidthToSelection} from '../helper/style-path'; import {applyColorToSelection, applyStrokeWidthToSelection} from '../helper/style-path';
class StrokeColorIndicator extends React.Component { class StrokeColorIndicator extends React.Component {
constructor (props) { constructor (props) {
@ -40,8 +40,13 @@ class StrokeColorIndicator extends React.Component {
this.props.onChangeStrokeWidth(0); this.props.onChangeStrokeWidth(0);
} }
// Apply color and update redux, but do not update svg until picker closes. // Apply color and update redux, but do not update svg until picker closes.
this._hasChanged = this._hasChanged = applyColorToSelection(
applyStrokeColorToSelection(newColor, isBitmap(this.props.format), this.props.textEditTarget) || newColor,
0, // colorIndex,
true, // isSolidGradient
isBitmap(this.props.format),
true, // applyToStroke
this.props.textEditTarget) ||
this._hasChanged; this._hasChanged;
this.props.onChangeStrokeColor(newColor); this.props.onChangeStrokeColor(newColor);
} }

View file

@ -7,7 +7,7 @@ import {changeStrokeColor} from '../reducers/stroke-color';
import {changeStrokeWidth} from '../reducers/stroke-width'; import {changeStrokeWidth} from '../reducers/stroke-width';
import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx';
import {getSelectedLeafItems} from '../helper/selection'; import {getSelectedLeafItems} from '../helper/selection';
import {applyStrokeColorToSelection, applyStrokeWidthToSelection, getColorsFromSelection, MIXED} import {applyColorToSelection, applyStrokeWidthToSelection, getColorsFromSelection, MIXED}
from '../helper/style-path'; from '../helper/style-path';
import Modes from '../lib/modes'; import Modes from '../lib/modes';
import Formats from '../lib/format'; import Formats from '../lib/format';
@ -25,7 +25,13 @@ class StrokeWidthIndicator extends React.Component {
if ((!this.props.strokeWidth || this.props.strokeWidth === 0) && newWidth > 0) { if ((!this.props.strokeWidth || this.props.strokeWidth === 0) && newWidth > 0) {
let currentColor = getColorsFromSelection(getSelectedLeafItems(), isBitmap(this.props.format)).strokeColor; let currentColor = getColorsFromSelection(getSelectedLeafItems(), isBitmap(this.props.format)).strokeColor;
if (currentColor === null) { if (currentColor === null) {
changed = applyStrokeColorToSelection('#000', isBitmap(this.props.format), this.props.textEditTarget) || changed = applyColorToSelection(
'#000',
0, // colorIndex,
true, // isSolidGradient
isBitmap(this.props.format),
true, // applyToStroke
this.props.textEditTarget) ||
changed; changed;
currentColor = '#000'; currentColor = '#000';
} else if (currentColor !== MIXED) { } else if (currentColor !== MIXED) {

View file

@ -7,13 +7,13 @@ import GradientTypes from '../lib/gradient-types';
import parseColor from 'parse-color'; import parseColor from 'parse-color';
import {DEFAULT_COLOR} from '../reducers/fill-style'; import {DEFAULT_COLOR} from '../reducers/fill-style';
import {isCompoundPathChild} from '../helper/compound-path'; import {isCompoundPathChild} from '../helper/compound-path';
import log from '../log/log';
const MIXED = 'scratch-paint/style-path/mixed'; const MIXED = 'scratch-paint/style-path/mixed';
// Check if the item color matches the incoming color. If the item color is a gradient, we assume // Check if the item color matches the incoming color. If the item color is a gradient, we assume
// that the incoming color never matches, since we don't support gradients yet. // that the incoming color never matches, since we don't support gradients yet.
const _colorMatch = function (itemColor, incomingColor) { const _colorMatch = function (itemColor, incomingColor) {
// @todo colorMatch should not be called with gradients as arguments once stroke gradients are supported
if (itemColor && itemColor.type === 'gradient') return false; if (itemColor && itemColor.type === 'gradient') return false;
// Either both are null or both are the same color when converted to CSS. // Either both are null or both are the same color when converted to CSS.
return (!itemColor && !incomingColor) || return (!itemColor && !incomingColor) ||
@ -62,7 +62,8 @@ const getRotatedColor = function (firstColor) {
* @param {?string} color2 CSS string, or null for transparent * @param {?string} color2 CSS string, or null for transparent
* @param {GradientType} gradientType gradient type * @param {GradientType} gradientType gradient type
* @param {paper.Rectangle} bounds Bounds of the object * @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 * @param {?paper.Point} [radialCenter] Where the center of a radial gradient should be, if the gradient is radial.
* Defaults to center of bounds.
* @return {paper.Color} Color object with gradient, may be null or color string if the gradient type is solid * @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) { const createGradientObject = function (color1, color2, gradientType, bounds, radialCenter) {
@ -74,7 +75,7 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad
color2 = getColorStringForTransparent(color1); color2 = getColorStringForTransparent(color1);
} }
const halfLongestDimension = Math.max(bounds.width, bounds.height) / 2; const halfLongestDimension = Math.max(bounds.width, bounds.height) / 2;
const start = gradientType === GradientTypes.RADIAL ? radialCenter : const start = gradientType === GradientTypes.RADIAL ? (radialCenter || bounds.center) :
gradientType === GradientTypes.VERTICAL ? bounds.topCenter : gradientType === GradientTypes.VERTICAL ? bounds.topCenter :
gradientType === GradientTypes.HORIZONTAL ? bounds.leftCenter : gradientType === GradientTypes.HORIZONTAL ? bounds.leftCenter :
null; null;
@ -93,17 +94,25 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad
}; };
/** /**
* Called when setting fill color * Called when setting an item's color
* @param {string} colorString color, css format, or null if completely transparent * @param {string} colorString color, css format, or null if completely transparent
* @param {number} colorIndex index of color being changed * @param {number} colorIndex index of color being changed
* @param {boolean} isSolidGradient True if is solid gradient. Sometimes the item has a gradient but the color * @param {boolean} isSolidGradient True if is solid gradient. Sometimes the item has a gradient but the color
* picker is set to a solid gradient. This happens when a mix of colors and gradient types is selected. * picker is set to a solid gradient. This happens when a mix of colors and gradient types is selected.
* When changing the color in this case, the solid gradient should override the existing gradient on the item. * When changing the color in this case, the solid gradient should override the existing gradient on the item.
* @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode * @param {?boolean} bitmapMode True if the color is being set in bitmap mode
* @param {?boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly. * @return {boolean} Whether the color application actually changed visibly.
*/ */
const applyFillColorToSelection = function (colorString, colorIndex, isSolidGradient, bitmapMode, textEditTargetId) { const applyColorToSelection = function (
colorString,
colorIndex,
isSolidGradient,
bitmapMode,
applyToStroke,
textEditTargetId
) {
const items = _getColorStateListeners(textEditTargetId); const items = _getColorStateListeners(textEditTargetId);
let changed = false; let changed = false;
for (let item of items) { for (let item of items) {
@ -112,40 +121,51 @@ const applyFillColorToSelection = function (colorString, colorIndex, isSolidGrad
} }
// In bitmap mode, fill color applies to the stroke if there is a stroke // In bitmap mode, fill color applies to the stroke if there is a stroke
if (bitmapMode && item.strokeColor !== null && item.strokeWidth) { if (
bitmapMode &&
!applyToStroke &&
item.strokeColor !== null &&
item.strokeWidth
) {
if (!_colorMatch(item.strokeColor, colorString)) { if (!_colorMatch(item.strokeColor, colorString)) {
changed = true; changed = true;
item.strokeColor = colorString; item.strokeColor = colorString;
} }
} else if (isSolidGradient || !item.fillColor || !item.fillColor.gradient || continue;
!item.fillColor.gradient.stops.length === 2) { }
const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor';
const itemColor = item[itemColorProp];
if (isSolidGradient || !itemColor || !itemColor.gradient ||
!itemColor.gradient.stops.length === 2) {
// Applying a solid color // Applying a solid color
if (!_colorMatch(item.fillColor, colorString)) { if (!_colorMatch(itemColor, colorString)) {
changed = true; changed = true;
if (isPointTextItem(item) && !colorString) { if (isPointTextItem(item) && !colorString) {
// Allows transparent text to be hit // Allows transparent text to be hit
item.fillColor = 'rgba(0,0,0,0)'; item[itemColorProp] = 'rgba(0,0,0,0)';
} else { } else {
item.fillColor = colorString; item[itemColorProp] = colorString;
} }
} }
} else if (!_colorMatch(item.fillColor.gradient.stops[colorIndex].color, colorString)) { } else if (!_colorMatch(itemColor.gradient.stops[colorIndex].color, colorString)) {
// Changing one color of an existing gradient // Changing one color of an existing gradient
changed = true; changed = true;
const otherIndex = colorIndex === 0 ? 1 : 0; const otherIndex = colorIndex === 0 ? 1 : 0;
if (colorString === null) { if (colorString === null) {
colorString = getColorStringForTransparent(item.fillColor.gradient.stops[otherIndex].color.toCSS()); colorString = getColorStringForTransparent(itemColor.gradient.stops[otherIndex].color.toCSS());
} }
const colors = [0, 0]; const colors = [0, 0];
colors[colorIndex] = colorString; colors[colorIndex] = colorString;
// If the other color is transparent, its RGB values need to be adjusted for the gradient to be smooth // If the other color is transparent, its RGB values need to be adjusted for the gradient to be smooth
if (item.fillColor.gradient.stops[otherIndex].color.alpha === 0) { if (itemColor.gradient.stops[otherIndex].color.alpha === 0) {
colors[otherIndex] = getColorStringForTransparent(colorString); colors[otherIndex] = getColorStringForTransparent(colorString);
} else { } else {
colors[otherIndex] = item.fillColor.gradient.stops[otherIndex].color.toCSS(); colors[otherIndex] = itemColor.gradient.stops[otherIndex].color.toCSS();
} }
// There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient. // There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient.
item.fillColor.gradient = {stops: colors, radial: item.fillColor.gradient.radial}; itemColor.gradient = {stops: colors, radial: itemColor.gradient.radial};
} }
} }
return changed; return changed;
@ -154,10 +174,13 @@ const applyFillColorToSelection = function (colorString, colorIndex, isSolidGrad
/** /**
* Called to swap gradient colors * Called to swap gradient colors
* @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode * @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode
* @param {?boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly. * @return {boolean} Whether the color application actually changed visibly.
*/ */
const swapColorsInSelection = function (bitmapMode, textEditTargetId) { const swapColorsInSelection = function (bitmapMode, applyToStroke, textEditTargetId) {
if (bitmapMode) return; // @todo
const items = _getColorStateListeners(textEditTargetId); const items = _getColorStateListeners(textEditTargetId);
let changed = false; let changed = false;
for (const item of items) { for (const item of items) {
@ -166,21 +189,19 @@ const swapColorsInSelection = function (bitmapMode, textEditTargetId) {
// that would leave us right where we started. // that would leave us right where we started.
if (isCompoundPathChild(item)) continue; if (isCompoundPathChild(item)) continue;
if (bitmapMode) { const itemColor = applyToStroke ? item.strokeColor : item.fillColor;
// @todo if (!itemColor || !itemColor.gradient || !itemColor.gradient.stops.length === 2) {
return;
} else if (!item.fillColor || !item.fillColor.gradient || !item.fillColor.gradient.stops.length === 2) {
// Only one color; nothing to swap // Only one color; nothing to swap
continue; continue;
} else if (!item.fillColor.gradient.stops[0].color.equals(item.fillColor.gradient.stops[1].color)) { } else if (!itemColor.gradient.stops[0].color.equals(itemColor.gradient.stops[1].color)) {
// Changing one color of an existing gradient // Changing one color of an existing gradient
changed = true; changed = true;
const colors = [ const colors = [
item.fillColor.gradient.stops[1].color.toCSS(), itemColor.gradient.stops[1].color.toCSS(),
item.fillColor.gradient.stops[0].color.toCSS() itemColor.gradient.stops[0].color.toCSS()
]; ];
// There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient. // There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient.
item.fillColor.gradient = {stops: colors, radial: item.fillColor.gradient.radial}; itemColor.gradient = {stops: colors, radial: itemColor.gradient.radial};
} }
} }
return changed; return changed;
@ -190,10 +211,11 @@ const swapColorsInSelection = function (bitmapMode, textEditTargetId) {
* Called when setting gradient type * Called when setting gradient type
* @param {GradientType} gradientType gradient type * @param {GradientType} gradientType gradient type
* @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode * @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode
* @param {boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly. * @return {boolean} Whether the color application actually changed visibly.
*/ */
const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEditTargetId) { const applyGradientTypeToSelection = function (gradientType, bitmapMode, applyToStroke, textEditTargetId) {
const items = _getColorStateListeners(textEditTargetId); const items = _getColorStateListeners(textEditTargetId);
let changed = false; let changed = false;
for (let item of items) { for (let item of items) {
@ -201,40 +223,45 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi
item = item.parent; item = item.parent;
} }
const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor';
const itemColor = item[itemColorProp];
const hasGradient = itemColor && itemColor.gradient;
let itemColor1; let itemColor1;
if (item.fillColor === null || item.fillColor.alpha === 0) { if (itemColor === null || itemColor.alpha === 0) {
// Transparent // Transparent
itemColor1 = null; itemColor1 = null;
} else if (!item.fillColor.gradient) { } else if (!hasGradient) {
// Solid color // Solid color
itemColor1 = item.fillColor.toCSS(); itemColor1 = itemColor.toCSS();
} else if (!item.fillColor.gradient.stops[0] || item.fillColor.gradient.stops[0].color.alpha === 0) { } else if (!itemColor.gradient.stops[0] || itemColor.gradient.stops[0].color.alpha === 0) {
// Gradient where first color is transparent // Gradient where first color is transparent
itemColor1 = null; itemColor1 = null;
} else { } else {
// Gradient where first color is not transparent // Gradient where first color is not transparent
itemColor1 = item.fillColor.gradient.stops[0].color.toCSS(); itemColor1 = itemColor.gradient.stops[0].color.toCSS();
} }
let itemColor2; let itemColor2;
if (!item.fillColor || !item.fillColor.gradient || !item.fillColor.gradient.stops[1]) { if (!hasGradient || !itemColor.gradient.stops[1]) {
// If item color is solid or a gradient that has no 2nd color, set the 2nd color based on the first color // If item color is solid or a gradient that has no 2nd color, set the 2nd color based on the first color
itemColor2 = getRotatedColor(itemColor1); itemColor2 = getRotatedColor(itemColor1);
} else if (item.fillColor.gradient.stops[1].color.alpha === 0) { } else if (itemColor.gradient.stops[1].color.alpha === 0) {
// Gradient has 2nd color which is transparent // Gradient has 2nd color which is transparent
itemColor2 = null; itemColor2 = null;
} else { } else {
// Gradient has 2nd color which is not transparent // Gradient has 2nd color which is not transparent
itemColor2 = item.fillColor.gradient.stops[1].color.toCSS(); itemColor2 = itemColor.gradient.stops[1].color.toCSS();
} }
if (bitmapMode) { if (bitmapMode) {
// @todo Add when we apply gradients to selections in bitmap mode // @todo Add when we apply gradients to selections in bitmap mode
continue; continue;
} else if (gradientType === GradientTypes.SOLID) { } else if (gradientType === GradientTypes.SOLID) {
if (item.fillColor && item.fillColor.gradient) { if (itemColor && itemColor.gradient) {
changed = true; changed = true;
item.fillColor = itemColor1; item[itemColorProp] = itemColor1;
} }
continue; continue;
} }
@ -245,12 +272,15 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi
if (itemColor2 === null) { if (itemColor2 === null) {
itemColor2 = getColorStringForTransparent(itemColor1); itemColor2 = getColorStringForTransparent(itemColor1);
} }
if (gradientType === GradientTypes.RADIAL) {
const hasRadialGradient = item.fillColor && item.fillColor.gradient && item.fillColor.gradient.radial; switch (gradientType) {
case GradientTypes.RADIAL: {
const hasRadialGradient = hasGradient && itemColor.gradient.radial;
if (!hasRadialGradient) { if (!hasRadialGradient) {
changed = true; changed = true;
const halfLongestDimension = Math.max(item.bounds.width, item.bounds.height) / 2; const halfLongestDimension = Math.max(item.bounds.width, item.bounds.height) / 2;
item.fillColor = {
item[itemColorProp] = {
gradient: { gradient: {
stops: [itemColor1, itemColor2], stops: [itemColor1, itemColor2],
radial: true radial: true
@ -259,13 +289,15 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi
destination: item.position.add(new paper.Point(halfLongestDimension, 0)) destination: item.position.add(new paper.Point(halfLongestDimension, 0))
}; };
} }
} else if (gradientType === GradientTypes.HORIZONTAL) { break;
const hasHorizontalGradient = item.fillColor && item.fillColor.gradient && }
!item.fillColor.gradient.radial && case GradientTypes.HORIZONTAL: {
Math.abs(item.fillColor.origin.y - item.fillColor.destination.y) < 1e-8; const hasHorizontalGradient = hasGradient && !itemColor.gradient.radial &&
Math.abs(itemColor.origin.y - itemColor.destination.y) < 1e-8;
if (!hasHorizontalGradient) { if (!hasHorizontalGradient) {
changed = true; changed = true;
item.fillColor = {
item[itemColorProp] = {
gradient: { gradient: {
stops: [itemColor1, itemColor2] stops: [itemColor1, itemColor2]
}, },
@ -273,12 +305,15 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi
destination: item.bounds.rightCenter destination: item.bounds.rightCenter
}; };
} }
} else if (gradientType === GradientTypes.VERTICAL) { break;
const hasVerticalGradient = item.fillColor && item.fillColor.gradient && !item.fillColor.gradient.radial && }
Math.abs(item.fillColor.origin.x - item.fillColor.destination.x) < 1e-8; case GradientTypes.VERTICAL: {
const hasVerticalGradient = hasGradient && !itemColor.gradient.radial &&
Math.abs(itemColor.origin.x - itemColor.destination.x) < 1e-8;
if (!hasVerticalGradient) { if (!hasVerticalGradient) {
changed = true; changed = true;
item.fillColor = {
item[itemColorProp] = {
gradient: { gradient: {
stops: [itemColor1, itemColor2] stops: [itemColor1, itemColor2]
}, },
@ -286,31 +321,8 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi
destination: item.bounds.bottomCenter destination: item.bounds.bottomCenter
}; };
} }
break;
} }
}
return changed;
};
/**
* Called when setting stroke color
* @param {string} colorString New color, css format
* @param {?boolean} bitmapMode True if the stroke color is being set in bitmap mode
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly.
*/
const applyStrokeColorToSelection = function (colorString, bitmapMode, textEditTargetId) {
// Bitmap mode doesn't have stroke color
if (bitmapMode) return false;
const items = _getColorStateListeners(textEditTargetId);
let changed = false;
for (let item of items) {
if (item.parent instanceof paper.CompoundPath) {
item = item.parent;
}
if (!_colorMatch(item.strokeColor, colorString)) {
changed = true;
item.strokeColor = colorString;
} }
} }
return changed; return changed;
@ -339,6 +351,33 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) {
return changed; return changed;
}; };
const _colorStateFromGradient = gradient => {
const colorState = {};
// Scratch only recognizes 2 color gradients
if (gradient.stops.length === 2) {
if (gradient.radial) {
colorState.gradientType = GradientTypes.RADIAL;
} else {
// Always use horizontal for linear gradients, since horizontal and vertical gradients
// are the same with rotation. We don't want to show MIXED just because anything is rotated.
colorState.gradientType = GradientTypes.HORIZONTAL;
}
colorState.primary = gradient.stops[0].color.alpha === 0 ?
null :
gradient.stops[0].color.toCSS();
colorState.secondary = gradient.stops[1].color.alpha === 0 ?
null :
gradient.stops[1].color.toCSS();
} else {
if (gradient.stops.length < 2) log.warn(`Gradient has ${gradient.stops.length} stop(s)`);
colorState.primary = MIXED;
colorState.secondary = MIXED;
}
return colorState;
};
/** /**
* Get state of colors and stroke width for selection * Get state of colors and stroke width for selection
* @param {!Array<paper.Item>} selectedItems Selected paper items * @param {!Array<paper.Item>} selectedItems Selected paper items
@ -349,12 +388,15 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) {
* Thickness is line thickness, used in the bitmap editor * Thickness is line thickness, used in the bitmap editor
*/ */
const getColorsFromSelection = function (selectedItems, bitmapMode) { const getColorsFromSelection = function (selectedItems, bitmapMode) {
// TODO: DRY out this code
let selectionFillColorString; let selectionFillColorString;
let selectionFillColor2String; let selectionFillColor2String;
let selectionStrokeColorString; let selectionStrokeColorString;
let selectionStrokeColor2String;
let selectionStrokeWidth; let selectionStrokeWidth;
let selectionThickness; let selectionThickness;
let selectionGradientType; let selectionFillGradientType;
let selectionStrokeGradientType;
let firstChild = true; let firstChild = true;
for (let item of selectedItems) { for (let item of selectedItems) {
@ -365,7 +407,9 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
let itemFillColorString; let itemFillColorString;
let itemFillColor2String; let itemFillColor2String;
let itemStrokeColorString; let itemStrokeColorString;
let itemGradientType = GradientTypes.SOLID; let itemStrokeColor2String;
let itemFillGradientType = GradientTypes.SOLID;
let itemStrokeGradientType = GradientTypes.SOLID;
if (!isGroup(item)) { if (!isGroup(item)) {
if (item.fillColor) { if (item.fillColor) {
@ -373,25 +417,10 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
if (isPointTextItem(item) && item.fillColor.alpha === 0) { if (isPointTextItem(item) && item.fillColor.alpha === 0) {
itemFillColorString = null; itemFillColorString = null;
} else if (item.fillColor.type === 'gradient') { } else if (item.fillColor.type === 'gradient') {
// Scratch only recognizes 2 color gradients const {primary, secondary, gradientType} = _colorStateFromGradient(item.fillColor.gradient);
if (item.fillColor.gradient.stops.length === 2) { itemFillColorString = primary;
if (item.fillColor.gradient.radial) { itemFillColor2String = secondary;
itemGradientType = GradientTypes.RADIAL; itemFillGradientType = gradientType;
} else {
// Always use horizontal for linear gradients, since horizontal and vertical gradients
// are the same with rotation. We don't want to show MIXED just because anything is rotated.
itemGradientType = GradientTypes.HORIZONTAL;
}
itemFillColorString = item.fillColor.gradient.stops[0].color.alpha === 0 ?
null :
item.fillColor.gradient.stops[0].color.toCSS();
itemFillColor2String = item.fillColor.gradient.stops[1].color.alpha === 0 ?
null :
item.fillColor.gradient.stops[1].color.toCSS();
} else {
itemFillColorString = MIXED;
itemFillColor2String = MIXED;
}
} else { } else {
itemFillColorString = item.fillColor.alpha === 0 ? itemFillColorString = item.fillColor.alpha === 0 ?
null : null :
@ -399,15 +428,34 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
} }
} }
if (item.strokeColor) { if (item.strokeColor) {
// Stroke color is fill color in bitmap
if (bitmapMode) { if (item.strokeColor.type === 'gradient') {
itemFillColorString = item.strokeColor.toCSS(); const {primary, secondary, gradientType} = _colorStateFromGradient(item.strokeColor.gradient);
} else if (item.strokeColor.type === 'gradient') { const strokeColorString = primary;
itemStrokeColorString = MIXED; const strokeColor2String = secondary;
const strokeGradientType = gradientType;
// Stroke color is fill color in bitmap
if (bitmapMode) {
itemFillColorString = strokeColorString;
itemFillColor2String = strokeColor2String;
itemFillGradientType = strokeGradientType;
} else {
itemStrokeColorString = strokeColorString;
itemStrokeColor2String = strokeColor2String;
itemStrokeGradientType = strokeGradientType;
}
} else { } else {
itemStrokeColorString = item.strokeColor.alpha === 0 || !item.strokeWidth ? const strokeColorString = item.strokeColor.alpha === 0 || !item.strokeWidth ?
null : null :
item.strokeColor.toCSS(); item.strokeColor.toCSS();
// Stroke color is fill color in bitmap
if (bitmapMode) {
itemFillColorString = strokeColorString;
} else {
itemStrokeColorString = strokeColorString;
}
} }
} else { } else {
itemStrokeColorString = null; itemStrokeColorString = null;
@ -418,7 +466,9 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
selectionFillColorString = itemFillColorString; selectionFillColorString = itemFillColorString;
selectionFillColor2String = itemFillColor2String; selectionFillColor2String = itemFillColor2String;
selectionStrokeColorString = itemStrokeColorString; selectionStrokeColorString = itemStrokeColorString;
selectionGradientType = itemGradientType; selectionStrokeColor2String = itemStrokeColor2String;
selectionFillGradientType = itemFillGradientType;
selectionStrokeGradientType = itemStrokeGradientType;
selectionStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0; selectionStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0;
if (item.strokeWidth && item.data && item.data.zoomLevel) { if (item.strokeWidth && item.data && item.data.zoomLevel) {
selectionThickness = item.strokeWidth / item.data.zoomLevel; selectionThickness = item.strokeWidth / item.data.zoomLevel;
@ -430,14 +480,22 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
if (itemFillColor2String !== selectionFillColor2String) { if (itemFillColor2String !== selectionFillColor2String) {
selectionFillColor2String = MIXED; selectionFillColor2String = MIXED;
} }
if (itemGradientType !== selectionGradientType) { if (itemFillGradientType !== selectionFillGradientType) {
selectionGradientType = GradientTypes.SOLID; selectionFillGradientType = GradientTypes.SOLID;
selectionFillColorString = MIXED; selectionFillColorString = MIXED;
selectionFillColor2String = MIXED; selectionFillColor2String = MIXED;
} }
if (itemStrokeGradientType !== selectionStrokeGradientType) {
selectionStrokeGradientType = GradientTypes.SOLID;
selectionStrokeColorString = MIXED;
selectionStrokeColor2String = MIXED;
}
if (itemStrokeColorString !== selectionStrokeColorString) { if (itemStrokeColorString !== selectionStrokeColorString) {
selectionStrokeColorString = MIXED; selectionStrokeColorString = MIXED;
} }
if (itemStrokeColor2String !== selectionStrokeColor2String) {
selectionStrokeColor2String = MIXED;
}
const itemStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0; const itemStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0;
if (selectionStrokeWidth !== itemStrokeWidth) { if (selectionStrokeWidth !== itemStrokeWidth) {
selectionStrokeWidth = null; selectionStrokeWidth = null;
@ -445,27 +503,50 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
} }
} }
// Convert selection gradient type from horizontal to vertical if first item is exactly vertical // Convert selection gradient type from horizontal to vertical if first item is exactly vertical
if (selectedItems && selectedItems.length && selectionGradientType !== GradientTypes.SOLID) { // This is because up to this point, we assume all non-radial gradients are horizontal
// Otherwise, if there were a mix of horizontal/vertical gradient types in the selection, they would show as MIXED
// whereas we want them to show as horizontal (or vertical if the first item is vertical)
if (selectedItems && selectedItems.length) {
let firstItem = selectedItems[0]; let firstItem = selectedItems[0];
if (firstItem.parent instanceof paper.CompoundPath) firstItem = firstItem.parent; if (firstItem.parent instanceof paper.CompoundPath) firstItem = firstItem.parent;
const direction = firstItem.fillColor.destination.subtract(firstItem.fillColor.origin);
if (Math.abs(direction.angle) === 90) { if (selectionFillGradientType !== GradientTypes.SOLID) {
selectionGradientType = GradientTypes.VERTICAL; // Stroke color is fill color in bitmap if fill color is missing
// TODO: this whole "treat horizontal/vertical gradients specially" logic is janky; refactor at some point
const firstItemColor = (bitmapMode && firstItem.strokeColor) ? firstItem.strokeColor : firstItem.fillColor;
const direction = firstItemColor.destination.subtract(firstItemColor.origin);
if (Math.abs(direction.angle) === 90) {
selectionFillGradientType = GradientTypes.VERTICAL;
}
}
if (selectionStrokeGradientType !== GradientTypes.SOLID) {
const direction = firstItem.strokeColor.destination.subtract(firstItem.strokeColor.origin);
if (Math.abs(direction.angle) === 90) {
selectionStrokeGradientType = GradientTypes.VERTICAL;
}
} }
} }
if (bitmapMode) { if (bitmapMode) {
return { return {
fillColor: selectionFillColorString ? selectionFillColorString : null, fillColor: selectionFillColorString ? selectionFillColorString : null,
fillColor2: selectionFillColor2String ? selectionFillColor2String : null, fillColor2: selectionFillColor2String ? selectionFillColor2String : null,
gradientType: selectionGradientType, fillGradientType: selectionFillGradientType,
thickness: selectionThickness thickness: selectionThickness
}; };
} }
// Treat stroke gradients as MIXED
// TODO: remove this once stroke gradients are supported
if (selectionStrokeGradientType !== GradientTypes.SOLID) selectionStrokeColorString = MIXED;
return { return {
fillColor: selectionFillColorString ? selectionFillColorString : null, fillColor: selectionFillColorString ? selectionFillColorString : null,
fillColor2: selectionFillColor2String ? selectionFillColor2String : null, fillColor2: selectionFillColor2String ? selectionFillColor2String : null,
gradientType: selectionGradientType, fillGradientType: selectionFillGradientType,
strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null, strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null,
strokeColor2: selectionStrokeColor2String ? selectionStrokeColor2String : null,
strokeGradientType: selectionStrokeGradientType,
strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0 strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0
}; };
}; };
@ -510,9 +591,8 @@ const styleShape = function (path, options) {
}; };
export { export {
applyFillColorToSelection, applyColorToSelection,
applyGradientTypeToSelection, applyGradientTypeToSelection,
applyStrokeColorToSelection,
applyStrokeWidthToSelection, applyStrokeWidthToSelection,
createGradientObject, createGradientObject,
getColorsFromSelection, getColorsFromSelection,

View file

@ -241,7 +241,8 @@ class TextTool extends paper.Tool {
content: '', content: '',
font: this.font, font: this.font,
fontSize: 40, fontSize: 40,
// TODO: style using gradient? // TODO: style using gradient
// https://github.com/LLK/scratch-paint/issues/1164
fillColor: this.colorState.fillColor.primary, fillColor: this.colorState.fillColor.primary,
// Default leading for both the HTML text area and paper.PointText // Default leading for both the HTML text area and paper.PointText
// is 120%, but for some reason they are slightly off from each other. // is 120%, but for some reason they are slightly off from each other.

View file

@ -14,7 +14,7 @@ const reducer = makeColorStyleReducer({
defaultColor: DEFAULT_COLOR, defaultColor: DEFAULT_COLOR,
selectionPrimaryColorKey: 'fillColor', selectionPrimaryColorKey: 'fillColor',
selectionSecondaryColorKey: 'fillColor2', selectionSecondaryColorKey: 'fillColor2',
selectionGradientTypeKey: 'gradientType' selectionGradientTypeKey: 'fillGradientType'
}); });
// Action creators ================================== // Action creators ==================================